Skip to content

Commit

Permalink
User management (#2711)
Browse files Browse the repository at this point in the history
* Basic User management (#2529)

* Enforce password security un user schema

* Argon2 hash algorithm for user password

* Add fullname and email fields to user schema

* Mix phx gen json scaffolding for users

* Users context

* mix format

* User controller refactored for the new users context

* Pow custom context for getting non-deleted users

* Unique email for users

* Update release task and seed for users

* Users context prevents deletion of user with id 1

* Username cannot be updated

* Created and updated timestamps in users view

* Change of the username when users are deleted

* Add lock feature to Users

* Locked and deleted users are excluded from renew

* Session controller tests updated

* /me endpoint returns also user id

* User socket authenticatio

* User controller broadcast user actions on user topic

* Access token expiration to 3 minutes

* Fix could not delete admin user in context

* Trento default admin enabled

* User controller with spex

* Fix app jwt auth plug test typo

* User controller actions are dispatched to per user channels

* fix access token test

* fix alias in factory file

* mix credo

* fix mispell

* mix credo

* removed comments from user_controller_test

* Addressing review feedbacks

* Introduced users factory

* Addresing feedbacks

* Updated trento_dev sql regression dump

* Revert "Updated trento_dev sql regression dump"

This reverts commit ccce860.

* Ecto seed with user password update on conflict

* Run db seed on regression tests

* Listening to user related events on the frontend (#2542)

* Frontend socket authentication refactored

* Add logged user in the state when user performs login

* Subscription to user topic on user login

* Guard component put user in state after me call

We need to enrich the user in the store not only on login
but also when the session is valid, we already call the me
endpoint in the guard to get user details, we store these details in the
state to have always consistency between the logged users on the backend
and the user in the store (returned from me endpoint at each app load)

* Listening on user actions event in the frontend

* Addressing review feedbacks

* Create Abilities and UserAbilities schema (#2543)

* mix.gen.context Abilities

* Users abilities

* Addressing review feedbacks

* Mix format

* Install make and gcc in container and rpm (#2553)

* Install make in container and rpm

* Install gcc

* Add New User Management View (#2544)

* Add users view

* Add users view test

* Add users story

* Refactor and rename code

* Refactor file structure and imports

* Refactor users component

* Refactor user component and delete unused views

* Enrich user's story

* Remove empty components

* Add banner and update test

* Refactor users page and users

* Disable Create button while loading users

* Address comments

* Fix storybook

* Refactor test and test for deletion api call

* Improve factory and texts

* order and clean up code

* Split up tests and clean up

* Address final pr comments

* Create user form (#2548)

* Move api validation errors code to lib

* Add users api

* Add users factory

* Pass props to password component

* Add UserForm view

* Add CreateUser view

* Add CreateUser to the router

* Update password policy tooltip

* Use a more standard file naming

* Create dedicated function on save clicked

* Rewrite test descriptions

* Policy enforcing on users (#2552)

* users context return user abilities

* LoadUserPlug load the user from the database using stateless user

* Wip on user controller policy

* Refactor abilities

* User controller renamed to users controller

* Refactored user openapi schemas

* Users controller policy usage refactored

* Add basic permissions

* Users controller abilities refactored

* mix credo

* addressing review feedbacks

* mix credo

* Fix policy test

* Edit user form (#2566)

* Update UserForm to allow edition style

* Add EditUserPage

* Fix conflicts after rebase resolution

* Correct some typos and descriptions

* User profile routes (#2583)

* Update user profile in users context

* profile controller wip

* Prevent profile update for admin user

* Profile controller

* Add current password validation to the user schema when profile updates
password

* Current password field in profile update schema

* Addressing review feedbacks

* mix credo

* Add soft delete for email and update test (#2584)

* Enable user status update (#2591)

* Enable locking up user in creation

* Enable status select in frontend

* Disable edit button for admin user (#2592)

* Abilities controller and update (#2568)

* Add abilities controller and index endpoint

* Add and update abilities in user operations

* Adapt user controller to use abilities

* Load abilities in profile endpoint

* Delete user abilities on deletion

* Optimistic lock for users (#2595)

* Optimistic locking on user schema update

* wip on preconditions

* mix credo

* Add optimistic locking in users controller

* Add optimistic locking handling in EditUserPage

* fix typo in users test

* fix user schema

* Add top-right corner profile button & menu (#2590)

* Store email from /me endpoint in state

* Add and use new Profile Menu component

* Use profile endpoint instead of /me

* Fix users/profile controller tests (#2607)

* Fix users controller test adding if-match

* Add admin user in profile controller test to avoid 403

* Add a generate password button for the UserForm (#2576)

* Add generate password button for input form

* Disable password generation button if user is admin

* Multi select component (#2600)

* Add react-select dependency

* Create MultiSelect component

* Add className to component

* Update classNames usage

* Update value containers color

* Switch to use link in ProfileMenu (#2608)

* Users abilities frontend (#2611)

* Add abilities api

* Load abilities and user abilities

* Update tests and stories

* Forbidden guard for user entries (#2612)

* Update redux code to load user abilities

* Implement the ForbiddenGuard

* User ForbiddenGuard in users related items

* Add missing abilities field in the user state

* Add disabled prop to multi-select (#2620)

* Interpolate validation errors in error view (#2616)

* User Profile page (#2615)

* Moved common forms labels and components to @lib/forms

* Wip profile page

* User profile form stories

* Profile password change form

* ProfileForm test

* ProfilePasswordChangeForm test

* ProfilePage test

* Close password dialog when user click save in profile view

* fix tests

* User profile form disabled when the user is the default admin

* Profile form modal toggle lift up

* Addressing review feedbacks

* Multi select for permissions in user profile

* ProfilePage dispatch setUser command in redux when user is updated

* Updated usage of multiselect in profile form

* Address review feedbacks

* User edit create toast (#2613)

* Add success and error toast

* Add toast test for create user page

* Add error toast if user is not found

* Rescue StaleError in user update function when abilities are present (#2622)

* Password change request (#2624)

* Add password_change_requested_at field and logic

* Add new field to controller and api spec

* Add maybe_ prefix to conditional functions

* Change password change requested to boolean and add to admin view

* Change placeholders in UserForm and EditUserPage(#2626)

* Password change request frontend (#2625)

* Move the toast out of profile menu and change z-index

* Move toaster inside router so it can use router data

* Load password requested field in redux store

* Add ToastNotifications component

* Create new customNotify action

* Add password change request fork in sagan index

* Dismiss password change request notification

* Use password_change_requested as boolean

* Update notifications to allow receiving health icons

* Clean and improve e2e performance (#2630)

* Remove unused commands

* Load initial state only if needed

* e2e user management (#2631)

* Update login commands for multi user usage

* Remove actions from users factory

* Add users e2e tests

* Admin cannot be edited test scenarios added

* Remove empty describe

* Add validation when a password is generated (#2640)

* Refactor generatePassword function

* Refactor test, update tooltip and enrich backend

* Enrich password test list

* User totp fields (#2646)

* Add new totp field migrations  to the user

* Add new fields to user schema

* Validate totp_enabled_at can only be nil for admin update

* Fix credo error

* Topt enrollment backend procedure (#2651)

* reset totp for users

* topt enrollment context

* topt openapi payloads

* Renamed reset totp

* Changed error when totp is invalid in the totp enrollment procedure

* TOTP Procedure in profile controller

* fix mispelling

* Addressing review feedbacks

* Login with totp (#2647)

* Add update_user_totp function

* Add totp validation code

* Update session controller to authenticate with totp

* Fix session controller create schema code

* Filter out totp_code from phoenix logs

* Send totp_code in users saga login action

* Add totp input state in login page

* Remove required fields to avoid backward incompatible error

* Use common Input in login page

* Fix tests after conflict resolution

* Totp enrollment frontend (#2657)

* Totp enrollment box component

* totp enabled field in profile controller response

* Profile totp enrollment frontend procedure

* fix mispell

* Confirmation modal on totp disable

* Addressing review feedbacks

* Allow admin disable totp (#2658)

* Allow update of totp_enabled field to false

* Allow disabling totp feature from user mgmt

* Adjust formatting

* Remove unnecessary changes and improve UserForm changes

* Various cleanups & move code to controller

* Move the totp disabling to the context

* Improve test description

* Improve UserForm component

* Fix missed ===

* Remove default padding in the Select component

* Improve TOTP select disabled check

* Add update_user test for TOTP disable

* Address remaining feedback

* Policy support functions (#2670)

* TOTP authentication e2e tests (#2667)

* Move lock user tests to new describe

* Add TOTP tests

* Skip long lasting test by default

* Disabled guard (#2672)

* Move ForbiddenGuard to common

* Create the DisabledGuard component

* Update early return usage

* Improve cloneElement usage

* Make tooltip place a prop

* Refactor storybook functions

* Some small improvements in the user forms (#2673)

* Remove TOTP configuration in user creation form

* Add disabled state to Switch component

* Disable TOTP configuration for default admin

* Style TOTP enrollment box (#2674)

* Style totp enrollment box usage in profile form

* Use profileFactory in profile form tests

* Reword multi-factor text usage

* Update e2e tests

* Use proper command to login (#2712)

* Use proper command to login

* Add page reloads to get correct suma posted data

* Set python3 as ansible interpreter (#2713)

* Set python3 as ansible interpreter

* Pin ubuntu version for pr env ci stages

---------

Co-authored-by: Carmine Di Monaco <[email protected]>
Co-authored-by: Eugen Maksymenko <[email protected]>
Co-authored-by: Rubén Torrero Marijnissen <[email protected]>
  • Loading branch information
4 people authored Jun 25, 2024
1 parent e3db6a0 commit 5788b52
Show file tree
Hide file tree
Showing 165 changed files with 10,112 additions and 352 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ jobs:
postgresql_version: "15"
- name: Run DB migrations
run: mix ecto.migrate
- name: Run DB seed
run: mix run priv/repo/seeds.exs
- name: Run trento detached
run: mix phx.server &
- name: Install photofinish
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/pr_env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:
jobs:
check_env_creation_privilege:
name: Check if the environment creation criteria are met, store in the job output
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
outputs:
create_env: ${{ steps.check.outputs.create_env }}
steps:
Expand All @@ -25,7 +25,7 @@ jobs:
build-and-push-pr-image:
needs: check_env_creation_privilege
name: Build and push pull request container image
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
if: needs.check_env_creation_privilege.outputs.create_env == 'true'
permissions:
contents: read
Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:

create_pr_environment:
name: Create or update the pr environment
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
needs: build-and-push-pr-image
env:
PR_BASE_URL: ${{ github.event.pull_request.number }}.prenv.trento.suse.com
Expand All @@ -88,6 +88,7 @@ jobs:
all:
vars:
ansible_user: ec2-user
ansible_python_interpreter: /usr/bin/python3
children:
trento-server:
hosts:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM opensuse/tumbleweed AS elixir-build
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
RUN zypper -n in git-core elixir elixir-hex erlang-rebar3
RUN zypper -n in make gcc git-core elixir elixir-hex erlang-rebar3
COPY . /build
WORKDIR /build
ARG MIX_ENV=prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function ApiKeySettingsModal({
</div>
<div className="w-2/4 pt-4">
<Select
className="pb-4"
optionsName=""
options={timeOptions}
disabled={apiKeyNeverExpires}
Expand Down
51 changes: 51 additions & 0 deletions assets/js/common/DisabledGuard/DisabledGuard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';

import Tooltip from '@common/Tooltip';

const ALL_PERMITTED = ['all:all'];

const DEFAULT_TOOLTIP_MESSAGE = 'You are not authorized for this action';

// DisabledGuard adds the `disabled` prop to the guarded element.
// The children element must be of one element.
// If the coming children is wrapped with a tooltip, the authorization tooltip
// supersedes the original tooltip in case of unauthorized access.

function DisabledGuard({
userAbilities,
permitted,
withTooltip = true,
tooltipMessage = DEFAULT_TOOLTIP_MESSAGE,
tooltipPlace = 'bottom',
children,
}) {
const permittedFor = ALL_PERMITTED.concat(permitted);

const isPermitted = userAbilities
.map(({ name, resource }) => `${name}:${resource}`)
.some((ability) => permittedFor.includes(ability));

if (isPermitted) {
return children;
}

const guardedElement =
children.type === Tooltip ? children.props.children : children;

const disabledElement = React.cloneElement(guardedElement, {
disabled: true,
});

return (
<Tooltip
isEnabled={withTooltip}
content={tooltipMessage}
place={tooltipPlace}
wrap={false}
>
<div>{disabledElement}</div>
</Tooltip>
);
}

export default DisabledGuard;
93 changes: 93 additions & 0 deletions assets/js/common/DisabledGuard/DisabledGuard.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';

import Button from '@common/Button';
import Tooltip from '@common/Tooltip';
import { PLACES } from '@common/Tooltip/Tooltip';

import DisabledGuard from '.';

export default {
title: 'Components/DisabledGuard',
component: DisabledGuard,
argTypes: {
userAbilities: {
control: { type: 'object' },
description: 'Current user abilities',
},
permitted: {
control: { type: 'object' },
description: 'Abilities that authorize the usage of the guarded element',
},
withTooltip: {
control: { type: 'boolean' },
description: 'Add tooltip saying user is not authorized for this action',
},
tooltipMessage: {
control: { type: 'text' },
description: 'Tooltip message',
},
tooltipPlace: {
control: { type: 'radio' },
options: PLACES,
description: 'Tooltip place',
},
},
};

const guardElement = (args) => (
<div className="pt-10 pl-32 w-64">
<DisabledGuard {...args}>
<Button>Click me!</Button>
</DisabledGuard>
</div>
);

const guardElementWithTooltip = (args) => (
<div className="pt-10 pl-32 w-64">
<DisabledGuard {...args}>
<Tooltip content="Original tooltip!">
<Button>Click me!</Button>
</Tooltip>
</DisabledGuard>
</div>
);

export const Authorized = {
args: {
userAbilities: [{ name: 'all', resource: 'all' }],
tooltipMessage: 'Some tooltip',
},
render: (args) => guardElement(args),
};

export const Disabled = {
args: {
...Authorized.args,
userAbilities: [],
permitted: ['action:resource'],
withTooltip: false,
},
render: (args) => guardElement(args),
};

export const DisabledWithTooltip = {
args: {
...Disabled.args,
withTooltip: true,
},
render: (args) => guardElement(args),
};

export const AuthorizedWithOriginalTooltip = {
args: {
...Authorized.args,
},
render: (args) => guardElementWithTooltip(args),
};

export const DisabledWithOriginalTooltip = {
args: {
...DisabledWithTooltip.args,
},
render: (args) => guardElementWithTooltip(args),
};
70 changes: 70 additions & 0 deletions assets/js/common/DisabledGuard/DisabledGuard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';

import { render, screen, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

import Button from '@common/Button';
import Tooltip from '@common/Tooltip';

import DisabledGuard from './DisabledGuard';

describe('DisabledGuard component', () => {
it('should disable the children component if the user is not authorized', () => {
render(
<DisabledGuard userAbilities={[]} permitted={['action:resource']}>
<Button>Click me!</Button>
</DisabledGuard>
);

expect(screen.getByRole('button')).toBeDisabled();
});

it('should not disable children component if the user is authorized', () => {
render(
<DisabledGuard
userAbilities={[{ name: 'action', resource: 'resource' }]}
permitted={['action:resource']}
>
<Button>Click me!</Button>
</DisabledGuard>
);

expect(screen.getByRole('button')).not.toBeDisabled();
});

it('should not disable children component if the user has all:all ability', () => {
render(
<DisabledGuard
userAbilities={[{ name: 'all', resource: 'all' }]}
permitted={['action:resource']}
>
<Button>Click me!</Button>
</DisabledGuard>
);

expect(screen.getByRole('button')).not.toBeDisabled();
});

it('should supersede the tooltip if the user is not authorized', async () => {
const user = userEvent.setup();

render(
<DisabledGuard userAbilities={[]} permitted={['action:resource']}>
<Tooltip content="This is my tooltip text" wrap={false}>
<Button>Click me!</Button>
</Tooltip>
</DisabledGuard>
);

expect(screen.getByRole('button')).toBeDisabled();

await act(async () => user.hover(screen.getByRole('button')));

await waitFor(() =>
expect(
screen.queryByText('You are not authorized for this action')
).toBeVisible()
);
});
});
3 changes: 3 additions & 0 deletions assets/js/common/DisabledGuard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DisabledGuard from './DisabledGuard';

export default DisabledGuard;
Loading

0 comments on commit 5788b52

Please sign in to comment.