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

Create customer should not require administrator rights #589

Merged
14 changes: 14 additions & 0 deletions e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
"e2e": "playwright test",
"e2e:ui": "playwright test --ui",
"e2e:report": "playwright show-report",
"record": "playwright codegen",
"test": "npm run start && npm run e2e && npm run stop"
},
"devDependencies": {
"@playwright/test": "^1.47.0",
"@types/node": "^22.5.4",
"wait-on": "^7.2.0"
},
"dependencies": {
"dotenv": "^16.4.5"
}
}
78 changes: 27 additions & 51 deletions e2e/playwright.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
// @ts-check
const { defineConfig, devices } = require('@playwright/test');
import { defineConfig, devices } from '@playwright/test';
import fs from 'node:fs';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config({ path: path.resolve(__dirname, '.env') });

const BROWSERS = ['Chrome', 'Firefox'];
const files = fs.readdirSync('tests', { withFileTypes: true });
const users = files.filter(file => file.isDirectory()).map(file => file.name);

// setup project
let projects = [{
name: 'setup',
testMatch: 'auth.setup.js',
}];

for (const browser of BROWSERS) {
for (const user of users) {
projects.push({
name: `${browser}:${user}`,
use: {
...devices[`Desktop ${browser}`],
storageState: `playwright/.auth/${user}.json`,
},
testDir: `./tests/${user}`,
dependencies: ['setup'],
})
}
}

/**
* @see https://playwright.dev/docs/test-configuration
*/
Expand All @@ -33,56 +58,7 @@ module.exports = defineConfig({
},

/* Configure projects for major browsers */
projects: [
// Setup project
{
name: 'setup',
testMatch: /.*\.setup\.js/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},

// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
projects: projects,

/* Run your local dev server before starting the tests */
// webServer: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from '@playwright/test';

test('successfully loads', async ({ page }) => {
test('create case with empty name should present error', async ({ page }) => {
await page.goto('/dashboard');

// FIXME: Should be a button instead of a link
Expand All @@ -10,4 +10,3 @@ test('successfully loads', async ({ page }) => {
// FIXME: Locator should be: page.getByRole('alert', { name: 'Invalid data type' });
await expect(page.getByText('Invalid data type')).toBeVisible();
});

7 changes: 7 additions & 0 deletions e2e/tests/administrator/manage/customers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';

test('should be able to open "Add customer" modal', async ({ page }) => {
await page.goto('/manage/customers');
await page.getByRole('button', { name: 'Add customer' }).click();
await expect(page.getByRole('heading', { name: 'Add customer' })).toBeVisible()
});
80 changes: 73 additions & 7 deletions e2e/tests/auth.setup.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,85 @@
import { test as setup } from '@playwright/test';
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import dotenv from 'dotenv';
import fs from 'node:fs';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');
const _API_URL = 'http://127.0.0.1:8000';

const username = "administrator";
const password = "MySuperAdminPassword!";
const _PERMISSION_CUSTOMERS_READ = 0x40;

const _ADMINISTRATOR_USERNAME = 'administrator';

setup('authenticate', async ({ page }) => {
let apiContext;
let administrator_password;

setup.beforeAll(async ({ playwright }) => {
const envFile = fs.readFileSync('../.env');
const env = dotenv.parse(envFile);

administrator_password = env.IRIS_ADM_PASSWORD

apiContext = await playwright.request.newContext({
baseURL: _API_URL,
extraHTTPHeaders: {
'Authorization': `Bearer ${env.IRIS_ADM_API_KEY}`,
'Content-Type': 'application/json'
},
});
});

async function authenticate(page, login, password) {
await page.goto('/');
await page.getByRole('textbox', { name: 'Username' }).fill(username);
await page.getByRole('textbox', { name: 'Username' }).fill(login);
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
// FIXME: It should be: await page.waitForURL('/dashboard'); No wildcard.
// Wait until the page receives the cookies.
await page.waitForURL('/dashboard*');
const authFile = path.join(__dirname, `../playwright/.auth/${login}.json`);
await page.context().storageState({ path: authFile });
});
}

setup('authenticate as administrator', async ({ page }) => {
await authenticate(page, _ADMINISTRATOR_USERNAME, administrator_password);
});

setup('authenticate as user with customers read rights', async ({ page }) => {
// TODO when this method is called a second time, all these request will fail
// think about a better ways of doing things, some possible strategies
// - find a way to create a new valid database before and empty the database after
// - find a way to remove elements from the database to roughly get back to the initial state
// - code so that these requests are robust (check the group exists, user exists, link between the two is set...)
// - global setup and teardown? https://playwright.dev/docs/test-global-setup-teardown
let response = await apiContext.post('/manage/groups/add', {
data: {
group_name: 'group_customers_r',
group_description: 'Group with rights: customers_read',
group_permissions: [_PERMISSION_CUSTOMERS_READ]
}
});
const groupIdentifier = (await response.json()).data.group_id;
const login = 'user_customers_r';
const password = 'aA.1234567890';
response = await apiContext.post('/manage/users/add', {
data: {
user_name: login,
user_login: login,
user_email: `${login}@eu`,
user_password: password
}
});
const userIdentifier = (await response.json()).data.id;
response = await apiContext.post(`/manage/users/${userIdentifier}/groups/update`, {
data: {
groups_membership: [groupIdentifier]
}
});

await authenticate(page, login, password);
});

setup.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});

7 changes: 7 additions & 0 deletions e2e/tests/user_customers_r/manage/customers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';

test('should be able to open "Add customer" modal', async ({ page }) => {
await page.goto('/manage/customers');
await page.getByRole('button', { name: 'Add customer' }).click();
await expect(page.getByRole('heading', { name: 'Add customer' })).toBeVisible()
});
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ def view_customer_modal(client_id, caseid, url_redir):

@manage_customers_blueprint.route('/manage/customers/add/modal', methods=['GET'])
@ac_requires(Permissions.customers_read, no_cid_required=True)
@ac_requires_client_access()
def add_customers_modal(caseid, url_redir):
if url_redir:
return redirect(url_for('manage_customers.manage_customers', cid=caseid))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ def view_customers(client_id):

@manage_customers_rest_blueprint.route('/manage/customers/add', methods=['POST'])
@ac_api_requires(Permissions.customers_write)
@ac_api_requires_client_access()
def add_customers():
if not request.is_json:
return response_error("Invalid request")
Expand Down
8 changes: 4 additions & 4 deletions source/app/blueprints/rest/manage/manage_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ def manage_groups_index():
def manage_groups_add():

if not request.is_json:
return response_error("Invalid request, expecting JSON")
return response_error('Invalid request, expecting JSON')

data = request.get_json()
if not data:
return response_error("Invalid request, expecting JSON")
return response_error('Invalid request, expecting JSON')

ags = AuthorizationGroupSchema()

Expand All @@ -85,9 +85,9 @@ def manage_groups_add():
db.session.commit()

except marshmallow.exceptions.ValidationError as e:
return response_error(msg="Data error", data=e.messages)
return response_error(msg='Data error', data=e.messages)

track_activity(message=f"added group {ags_c.group_name}", ctx_less=True)
track_activity(message=f'added group {ags_c.group_name}', ctx_less=True)

return response_success('', data=ags.dump(ags_c))

Expand Down
5 changes: 4 additions & 1 deletion tests/iris.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@
from user import User

API_URL = 'http://127.0.0.1:8000'
# TODO SSOT: this could be directly read from the .env file
_API_KEY = 'B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594'
_IRIS_PATH = Path('..')
_TEST_DATA_PATH = Path('./data')
_ADMINISTRATOR_USER_IDENTIFIER = 1


class Iris:

def __init__(self):
self._docker_compose = DockerCompose(_IRIS_PATH, 'docker-compose.dev.yml')
# TODO remove this field and use _administrator instead
self._api = RestApi(API_URL, _API_KEY)
self._administrator = User(API_URL, _API_KEY, 1)
self._administrator = User(API_URL, _API_KEY, _ADMINISTRATOR_USER_IDENTIFIER)
self._user_count = 0

def _wait(self, condition, attempts, sleep_duration=1):
Expand Down
18 changes: 18 additions & 0 deletions tests/tests_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
_INITIAL_DEMO_CASE_IDENTIFIER = 1
_GROUP_ANALYSTS_IDENTIFIER = 2
_CASE_ACCESS_LEVEL_FULL_ACCESS = 4
_PERMISSION_CUSTOMERS_WRITE = 0x80

# TODO should change None into 123456789 and maybe fix...
_IDENTIFIER_FOR_NONEXISTENT_OBJECT = None
Expand Down Expand Up @@ -571,3 +572,20 @@ def test_delete_task_should_return_403_when_user_has_insufficient_rights(self):

response = user.delete(f'/api/v2/tasks/{task_identifier}')
self.assertEqual(403, response.status_code)

def test_create_customer_should_return_200_when_user_has_customer_write_right(self):
body = {
'group_name': 'Customer create',
'group_description': 'Group with customers_write right',
'group_permissions': [_PERMISSION_CUSTOMERS_WRITE]
}
response = self._subject.create('/manage/groups/add', body).json()
group_identifier = response['data']['group_id']
user = self._subject.create_dummy_user()
body = {'groups_membership': [group_identifier]}
self._subject.create(f'/manage/users/{user.get_identifier()}/groups/update', body)

body = {'custom_attributes': {}, 'customer_description': '', 'customer_name': 'Customer', 'customer_sla': ''}
response = user.create('/manage/customers/add', body)

self.assertEqual(200, response.status_code)
2 changes: 1 addition & 1 deletion tests/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ def get(self, path):
return self._api.get(path)

def delete(self, path):
return self._api.delete(path)
return self._api.delete(path)
Loading