Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invite users via email #4539

Merged
merged 83 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
ffe5546
feat: add email address to fides_user model
RobertKeyser Dec 20, 2023
3b82feb
chore: create Alembic migration for email_address
RobertKeyser Dec 20, 2023
3aeaeed
Adding email messaging status to health check endpoint
galvana Dec 20, 2023
133d46f
feat: add email_address to user API schema
RobertKeyser Dec 20, 2023
8a34303
fix: comment referenced incorrect type
RobertKeyser Dec 20, 2023
b8dc6d9
feat: add test for invalid email
RobertKeyser Dec 20, 2023
ae4ab39
fix: CITEXT in alembic migration and remove unrelated migration
RobertKeyser Dec 20, 2023
4a13237
Merge branch 'rkeyser/email-address' into invite-users-via-email
galvana Dec 21, 2023
52d8e26
Adding Fides user invite table
galvana Dec 21, 2023
15d28f7
Update user types
allisonking Dec 21, 2023
c767b38
Add email address fields to user management pages
allisonking Dec 21, 2023
a662ab4
Update cypress fixtures
allisonking Dec 21, 2023
79d5228
fix: add `email_address` to user endpoint tests
RobertKeyser Dec 21, 2023
0f7dda4
feat: add API constraint to prevent duplicate email addresses
RobertKeyser Dec 21, 2023
1812469
Adding user invite messaging action
galvana Dec 21, 2023
478ee3d
feat: add disabled and disabled_reason to database
RobertKeyser Dec 21, 2023
cd4f5d9
feat: add ability to pass email in while creating user via CLI
RobertKeyser Dec 21, 2023
a5a3d70
feat: prevent access token if user disabled
RobertKeyser Dec 21, 2023
5877850
Merge branch 'invite-users-via-email' into rkeyser/email-address
RobertKeyser Dec 21, 2023
66c70b7
Conditionally render invite sign in page
allisonking Dec 21, 2023
e8f582d
fix: user endpoint tests - disabled constraints
RobertKeyser Dec 21, 2023
ff9c006
fix: remove assertion for optional field
RobertKeyser Dec 21, 2023
31ea3aa
Adding stub for user accept invite endpoint
galvana Dec 21, 2023
618232d
Merge branch 'rkeyser/email-address' into invite-users-via-email
galvana Dec 21, 2023
510ed56
Add redirect when not logged in
allisonking Dec 21, 2023
ef22eda
Do not render password field if email is configured
allisonking Dec 21, 2023
7f6770d
fix: disabled=false in tests
RobertKeyser Dec 21, 2023
65c6b6b
Merge branch 'rkeyser/email-address' into invite-users-via-email
allisonking Dec 21, 2023
fe1e3ef
Add condition for not redirecting to login
allisonking Dec 21, 2023
859f728
Still show password form for now
allisonking Dec 21, 2023
0b59b8b
Adding accept invite endpoint
galvana Dec 21, 2023
ac2ee34
Fixing password fallback
galvana Dec 21, 2023
e93270c
Try to protect against redirects again
allisonking Dec 21, 2023
1d1814e
Allow creating user without a password
allisonking Dec 21, 2023
394e30f
Integrate with accept-invite endpoint
allisonking Dec 21, 2023
46ada12
Removing base64 decode
galvana Dec 21, 2023
6ff774c
Adding invite sent badges
galvana Dec 22, 2023
86ea173
Merge branch 'main' into invite-users-via-email
galvana Dec 22, 2023
3be4a86
Validate redirects and allow [id] paths to work
allisonking Dec 22, 2023
4b9c4d4
fix: missing email_address attribute in test
RobertKeyser Dec 22, 2023
1183169
Fix regex warning
allisonking Dec 22, 2023
ee542d5
Merge branch 'rkeyser/email-address' into invite-users-via-email
RobertKeyser Dec 22, 2023
a29342b
Secret login improvements 🤫 (#4541)
allisonking Dec 22, 2023
151408e
Updating tests
galvana Jan 5, 2024
d26b30e
Consolidating migration steps
galvana Jan 6, 2024
ff35dbb
Mypy fixes
galvana Jan 8, 2024
d37ee5b
Merge branch 'main' into invite-users-via-email
galvana Jan 8, 2024
5b203a3
Updating downrev
galvana Jan 8, 2024
7281f50
Merge branch 'main' into invite-users-via-email
allisonking Jan 9, 2024
dc81555
Update changelog
allisonking Jan 9, 2024
c9ac6a2
Add login page tests
allisonking Jan 9, 2024
2d577a2
Do not show animation if user prefers reduced motion
allisonking Jan 9, 2024
e4917de
Configure cypress to run with reduced motion preference
allisonking Jan 9, 2024
3026dce
Update a few more types
allisonking Jan 9, 2024
fc2c398
Add tests for invite sent badge and conditional password field rendering
allisonking Jan 9, 2024
5d823c1
Misc backend fixes
galvana Jan 10, 2024
490b17e
Updating dataset and cli test
galvana Jan 10, 2024
832d2a5
Removing unnecessary disabled field
galvana Jan 10, 2024
97ddd64
Removing unnecessary field from fixture
galvana Jan 10, 2024
d45dc93
Consolidating conditions for inviting users via email
galvana Jan 10, 2024
2d2ceee
Merge branch 'main' into invite-users-via-email
allisonking Jan 11, 2024
cf01940
Merging in main
galvana Jan 23, 2024
74ac32f
Merge branch 'main' into invite-users-via-email
allisonking Jan 26, 2024
1d4f176
Merge branch 'main' into invite-users-via-email
galvana Feb 1, 2024
b579d77
Updating migration
galvana Feb 1, 2024
76c4761
Changes based on PR feedback
galvana Mar 8, 2024
47d89d0
Merge branch 'main' into invite-users-via-email
galvana Mar 8, 2024
f44fa65
Misc cleanup
galvana Mar 8, 2024
0f453c3
Fixing downrev
galvana Mar 8, 2024
006bb07
Merge branch 'main' into invite-users-via-email
galvana Jun 13, 2024
c21b267
Fixing downrev
galvana Jun 13, 2024
1a32f8a
Merge branch 'main' into invite-users-via-email
galvana Jun 25, 2024
af09090
Fixing downrev
galvana Jun 25, 2024
d1cc308
UI import fixes
galvana Jun 26, 2024
13437da
Fixing Cypress test
galvana Jun 26, 2024
d81cceb
Removing usage of mocks for endpoint tests
galvana Jun 26, 2024
1e3b666
Misc cleanup
galvana Jun 26, 2024
4488b94
Merge branch 'main' into invite-users-via-email
galvana Jun 26, 2024
9a8d8e4
Merge branch 'main' into invite-users-via-email
galvana Jul 1, 2024
ffc57d8
Changes based on PR feedback
galvana Jul 2, 2024
1d2d42a
Re-adding comma
galvana Jul 2, 2024
6aff7d3
Fixing tests
galvana Jul 2, 2024
9b3e248
Updating migration script
galvana Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .fides/db_dataset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ dataset:
- system.operations
- name: first_name
data_categories:
- system.operations
- user.name.first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoa! thorough stuff. don't tell me you guys did this in the hackathon... 👀

- name: hashed_password
data_categories:
- system.operations
Expand All @@ -450,7 +450,7 @@ dataset:
- system.operations
- name: last_name
data_categories:
- system.operations
- user.name.last
- name: password_reset_at
data_categories:
- system.operations
Expand All @@ -462,8 +462,17 @@ dataset:
data_categories:
- system.operations
- name: username
data_categories:
- user.account.username
- name: disabled
data_categories:
- system.operations
- name: disabled_reason
data_categories:
- system.operations
- name: email_address
data_categories:
- user.contact.email
- name: fidesuserpermissions
fields:
- name: created_at
Expand Down Expand Up @@ -2164,3 +2173,17 @@ dataset:
data_categories: [system]
- name: user_assigned_data_categories
data_categories: [system]
- name: fides_user_invite
fields:
- name: created_at
data_categories: [system.operations]
- name: hashed_invite_code
data_categories: [system.operations]
- name: id
data_categories: [system.operations]
- name: salt
data_categories: [system.operations]
- name: updated_at
data_categories: [system.operations]
- name: username
data_categories: [user.account.username]
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The types of changes are:
- New messaging page. Allows managing messaging templates for different properties. [#5005](https://github.com/ethyca/fides/pull/5005)
- Ability to configure "Enforcement Level" for Privacy Notices [#5025](https://github.com/ethyca/fides/pull/5025)
- BE cleanup for property-specific messaging [#5006](https://github.com/ethyca/fides/pull/5006)
- Invite users via email flow [#4539](https://github.com/ethyca/fides/pull/4539)

### Changed
- Navigation changes. 'Management' was renamed 'Settings'. Properties was moved to Settings section. [#5005](https://github.com/ethyca/fides/pull/5005)
Expand Down
9 changes: 9 additions & 0 deletions clients/admin-ui/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
setupNodeEvents(on) {
on("before:browser:launch", (browser, launchOptions) => {
if (browser.family === "chromium") {
// No need for tests to be slowed down by animations!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ - also does it matter what browser the cypress test uses? Can we just set this across all browsers for e2e testing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more that the --force-prefers-reduced-motion option might be chromium specific. I believe we only use electron (which I think is chromium based) and chrome though, so that should cover our use cases!

launchOptions.args.push("--force-prefers-reduced-motion");
}
return launchOptions;
});
},
},

defaultCommandTimeout: 5000,
Expand Down
63 changes: 54 additions & 9 deletions clients/admin-ui/cypress/e2e/auth.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { SYSTEM_ROUTE } from "~/features/common/nav/v2/routes";

describe("User Authentication", () => {
const login = () => {
cy.fixture("login.json").then((body) => {
cy.intercept("POST", "/api/v1/login", body).as("postLogin");
cy.intercept("/api/v1/user/*/permission", {
fixture: "user-management/permissions.json",
}).as("getUserPermission");
});

cy.getByTestId("input-username").type("[email protected]");
cy.getByTestId("input-password").type("FakePassword123!{Enter}");
};
describe("when the user not logged in", () => {
it("redirects them to the login page", () => {
cy.visit("/");
Expand All @@ -20,18 +31,20 @@ describe("User Authentication", () => {
cy.getByTestId("Login");

cy.intercept("GET", "/api/v1/system", { body: [] });
cy.fixture("login.json").then((body) => {
cy.intercept("POST", "/api/v1/login", body).as("postLogin");
cy.intercept("/api/v1/user/*/permission", {
fixture: "user-management/permissions.json",
}).as("getUserPermission");
});

cy.get("#email").type("[email protected]");
cy.get("#password").type("FakePassword123!{Enter}");
login();

cy.getByTestId("Home");
});

it("can persist URL after logging in", () => {
cy.visit("/user-management");
cy.location("pathname").should("eq", "/login");
cy.location("search").should("eq", "?redirect=%2Fuser-management");

// Now log in
login();
cy.location("pathname").should("eq", "/user-management");
});
});

describe("when the user is logged in", () => {
Expand Down Expand Up @@ -70,4 +83,36 @@ describe("User Authentication", () => {
cy.location("pathname").should("eq", "/");
});
});

describe("invited user", () => {
beforeEach(() => {
cy.intercept("/api/v1/user/*/permission", {
fixture: "user-management/permissions.json",
}).as("getUserPermission");
cy.fixture("login.json").then((body) => {
cy.intercept("POST", "/api/v1/user/accept-invite*", body).as(
"postAcceptInvite"
);
});
});
it("can prefill email and render different copy for an invited user", () => {
const data = { username: "testuser", invite_code: "123" };
const newPassword = "FakePassword123!";
cy.visit("/login", {
qs: data,
});
cy.getByTestId("input-username").should("be.disabled");
cy.getByTestId("input-username").should("have.value", data.username);
cy.get("label").contains("Set new password");
cy.getByTestId("input-password").type(newPassword);
cy.get("button").contains("Setup user").click();
cy.wait("@postAcceptInvite").then((interception) => {
const { body, url } = interception.request;
expect(url).to.contain(data.invite_code);
expect(url).to.contain(data.username);
expect(body).to.eql({ new_password: newPassword });
});
cy.getByTestId("Home");
});
});
});
29 changes: 29 additions & 0 deletions clients/admin-ui/cypress/e2e/user-management.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ describe("User management", () => {
cy.getByTestId("user-systems-badge");
cy.contains("4");
});

it("can see invite sent field", () => {
cy.visit("/user-management");
cy.wait("@getAllUsers");
cy.getByTestId(`row-${USER_1_ID}`).within(() => {
cy.getByTestId("invite-sent-badge");
});
cy.getByTestId(`row-${CYPRESS_USER_ID}`).within(() => {
cy.getByTestId("invite-sent-badge").should("not.exist");
});
});
});

describe("Create users", () => {
it("can set a user's password if email messaging is not configured", () => {
cy.visit(`/user-management/new`);
cy.getByTestId("input-password");
});

it("cannot set a user's password if email messaging is enabled", () => {
cy.intercept("GET", "**/messaging/email-invite/status", {
body: {
enabled: true,
},
});
cy.visit(`/user-management/new`);
cy.getByTestId("input-email_address");
cy.getByTestId("input-password").should("not.exist");
});
});

describe("Password management", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"id": "123",
"username": "[email protected]",
"email_address": "[email protected]",
"created_at": "2022-09-28T16:15:30.994Z",
"first_name": "Cypress",
"last_name": "User"
Expand Down
17 changes: 13 additions & 4 deletions clients/admin-ui/cypress/fixtures/user-management/users.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,39 @@
{
"id": "fid_bad38cda-476b-4d25-9883-798ba1415f40",
"username": "user_3",
"email_address": "[email protected]",
"created_at": "2023-01-26T16:24:50.718476+00:00",
"first_name": "User",
"last_name": "Three"
"last_name": "Three",
"disabled": false
},
{
"id": "fid_560cceb5-f567-4d76-a905-1e16ada1c143",
"username": "user_2",
"email_address": "[email protected]",
"created_at": "2023-01-26T16:23:56.023966+00:00",
"first_name": "User",
"last_name": "Two"
"last_name": "Two",
"disabled": false
},
{
"id": "fid_ee8f54ce-19f7-4640-b311-1cc1e77e7166",
"username": "user_1",
"email_address": "[email protected]",
"created_at": "2023-01-26T16:16:49.575653+00:00",
"first_name": "User",
"last_name": "One"
"last_name": "One",
"disabled": true,
"disabled_reason": "pending_invite"
},
{
"id": "123",
"username": "[email protected]",
"email_address": "[email protected]",
"created_at": "2022-09-28T16:15:30.994Z",
"first_name": "Cypress",
"last_name": "User"
"last_name": "User",
"disabled": false
}
],
"total": 4,
Expand Down
10 changes: 9 additions & 1 deletion clients/admin-ui/src/features/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useGetUserPermissionsQuery } from "~/features/user-management";

import { logout, selectToken, selectUser } from "./auth.slice";

const REDIRECT_IGNORES = ["/", "/login"];

const useProtectedRoute = (redirectUrl: string) => {
const router = useRouter();
const dispatch = useAppDispatch();
Expand All @@ -28,7 +30,13 @@ const useProtectedRoute = (redirectUrl: string) => {
dispatch(logout());
}
if (typeof window !== "undefined") {
router.push(redirectUrl);
const query = REDIRECT_IGNORES.includes(window.location.pathname)
? undefined
: { redirect: window.location.pathname };
router.push({
pathname: redirectUrl,
query,
});
}
return { authenticated: false, hasAccess: false };
}
Expand Down
12 changes: 12 additions & 0 deletions clients/admin-ui/src/features/auth/auth.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,24 @@ const authApi = baseApi.injectEndpoints({
query: () => ({ url: `oauth/role` }),
providesTags: ["Roles"],
}),
acceptInvite: build.mutation<
LoginResponse,
LoginRequest & { inviteCode: string }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does inviteCode get passed in here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah over here!

user = await acceptInviteRequest({
...credentials,
inviteCode,
}).unwrap();

>({
query: ({ username, password, inviteCode }) => ({
url: "/user/accept-invite",
params: { username, invite_code: inviteCode },
method: "POST",
body: { new_password: password },
}),
}),
}),
});

export const {
useLoginMutation,
useLogoutMutation,
useAcceptInviteMutation,
useGetRolesToScopesMappingQuery,
} = authApi;
export const { reducer } = authSlice;
90 changes: 90 additions & 0 deletions clients/admin-ui/src/features/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
Button,
Flex,
Link,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
QuestionIcon,
Stack,
Text,
UserIcon,
} from "fidesui";
import { useRouter } from "next/router";
import React from "react";

import { useAppDispatch, useAppSelector } from "~/app/hooks";
import { LOGIN_ROUTE } from "~/constants";
import { logout, selectUser, useLogoutMutation } from "~/features/auth";

const useHeader = () => {
const { username } = useAppSelector(selectUser) ?? { username: "" };
return { username };
};

const Header: React.FC = () => {
const { username } = useHeader();
const router = useRouter();
const [logoutMutation] = useLogoutMutation();
const dispatch = useAppDispatch();

const handleLogout = async () => {
await logoutMutation({});
// Go to Login page first, then dispatch logout so that ProtectedRoute does not
// tack on a redirect URL. We don't need a redirect URL if we are just logging out!
router.push(LOGIN_ROUTE).then(() => {
dispatch(logout());
});
};

return (
<Flex
as="header"
height={12}
width="100%"
paddingX={10}
flexShrink={0}
alignItems="center"
justifyContent="end"
backgroundColor="gray.50"
>
<Flex alignItems="center">
<Link href="https://docs.ethyca.com" isExternal>
<Button size="sm" variant="ghost">
<QuestionIcon color="gray.700" boxSize={4} />
</Button>
</Link>
{username && (
<Menu>
<MenuButton
as={Button}
size="sm"
variant="ghost"
data-testid="header-menu-button"
>
<UserIcon color="gray.700" />
</MenuButton>
<MenuList shadow="xl" zIndex="20">
<Stack px={3} py={2} spacing={1}>
<Text fontWeight="medium">{username}</Text>
</Stack>

<MenuDivider />
<MenuItem
_focus={{ color: "complimentary.500", bg: "gray.100" }}
onClick={handleLogout}
data-testid="header-menu-sign-out"
>
Sign out
</MenuItem>
</MenuList>
</Menu>
)}
</Flex>
</Flex>
);
};

export default Header;
1 change: 1 addition & 0 deletions clients/admin-ui/src/features/common/api.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const baseApi = createApi({
"Datasets",
"Discovery Monitor Configs",
"Discovery Monitor Results",
"Email Invite Status",
"Fides Cloud Config",
"Languages",
"Locations",
Expand Down
Loading
Loading