Skip to content

Commit

Permalink
[data_release] Introduce Project Separation (#9385)
Browse files Browse the repository at this point in the history
 Add project separation permissions for the data release module
- This makes it so that users do not see data_release files from
different projects.
- If a data_release file does not have a projectID, then all users with
access to that version/file can see it.
- Improved the manage permission form because it was not very user
friendly
- Version and user options only show up as the ones that the current
user has access to that project for
  • Loading branch information
skarya22 authored Oct 31, 2024
1 parent bf53274 commit 38266d4
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 55 deletions.
4 changes: 3 additions & 1 deletion SQL/0000-00-00-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2096,7 +2096,9 @@ CREATE TABLE `data_release` (
`file_name` varchar(255),
`version` varchar(255),
`upload_date` date,
PRIMARY KEY (`id`)
`ProjectID` INT(10) UNSIGNED NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (ProjectID) REFERENCES Project (ProjectID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `data_release_permissions` (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE data_release
ADD COLUMN ProjectID INT(10) UNSIGNED NULL DEFAULT NULL,
ADD CONSTRAINT FK_ProjectID
FOREIGN KEY (ProjectID) REFERENCES Project (ProjectID);
6 changes: 3 additions & 3 deletions modules/data_release/jsx/addPermissionForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ class AddPermissionForm extends Component {
* Called by React when the component has been rendered on the page.
*/
componentDidMount() {
this.fetchData()
.then(() => this.setState({isLoaded: true}));
this.fetchData().then(() => this.setState({isLoaded: true}));
}

/**
Expand Down Expand Up @@ -112,6 +111,7 @@ class AddPermissionForm extends Component {
errorMessage={this.state.errorMessage.Filename}
required={false}
value={this.state.formData.data_release_id}
autoSelect={false}
/>
<h4>OR</h4><br/>
<SelectElement
Expand All @@ -124,6 +124,7 @@ class AddPermissionForm extends Component {
errorMessage={this.state.errorMessage.Version}
required={false}
value={this.state.formData.data_release_version}
autoSelect={false}
/>
<ButtonElement label='Add Permission'/>
</FormElement>
Expand Down Expand Up @@ -181,7 +182,6 @@ class AddPermissionForm extends Component {
}).then(function() {
window.location.assign('/data_release');
});
this.props.fetchData();
} else {
let msg = response.statusText ?
response.statusText :
Expand Down
6 changes: 6 additions & 0 deletions modules/data_release/jsx/dataReleaseIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ class DataReleaseIndex extends Component {
name: 'uploadDate',
type: 'text',
}},
{label: 'Project Name', show: true, filter: {
name: 'Project',
type: 'select',
options: this.state.data.fieldOptions.projects,
}},
{label: 'Data Release ID', show: false,
},
];
Expand All @@ -178,6 +183,7 @@ class DataReleaseIndex extends Component {
+ '/data_release/files'
}
action={loris.BaseURL + '/data_release/files'}
projects={this.state.data.fieldOptions.projects}
/>
</Modal>
);
Expand Down
107 changes: 81 additions & 26 deletions modules/data_release/jsx/managePermissionsForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FormElement,
CheckboxElement,
StaticElement,
SearchableDropdown,
} from 'jsx/Form';

/**
Expand All @@ -24,11 +25,13 @@ class ManagePermissionsForm extends Component {

this.state = {
data: {},
originalData: {},
hasError: {},
errorMessage: {},
isLoaded: false,
};

this.setFormData = this.setFormData.bind(this);
this.fetchData = this.fetchData.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
Expand All @@ -49,7 +52,7 @@ class ManagePermissionsForm extends Component {
fetchData() {
return fetch(this.props.DataURL, {credentials: 'same-origin'})
.then((resp) => resp.json())
.then((data) => this.setState({data}))
.then((data) => this.setState({data, originalData: data}))
.catch( (error) => {
this.setState({error: 'An error occurred when loading the form!'});
console.error(error);
Expand Down Expand Up @@ -88,26 +91,72 @@ class ManagePermissionsForm extends Component {
onSubmit={this.handleSubmit}
>
<FormElement name="manage_permissions">
{Object.entries(data).map(([userId, user]) => {
const versions = Object.values(options.versions).map((version) =>
<div key={version}>
<SearchableDropdown name="user"
label="Manage Versions a User has access to"
placeHolder="Search for a User"
options={options.users}
strictSearch={true}
onUserInput={this.setFormData}
value={this.state.user}
/>
{this.state.user && <StaticElement
label={'Versions'}
text={Object.values(options.versions).map((version) =>
<div>
<CheckboxElement
name={version}
label={version || 'Unversioned'}
value={user.versions.includes(version)}
onUserInput={(version, permission) =>
this.setFormData(userId, version, permission)
name={'versionsByUser'}
label={version}
value={data[this.state.user].versions.includes(version)}
onUserInput={(formElement, checked) =>
this.setFormData(
'versionsByUser', {
userId: this.state.user, version, checked,
}
)
}
/><br/>
</div>
);

return <StaticElement
key={userId}
label={user.name}
text={<div>{versions}</div>}
/>;
})};
)}
/>
}
<SearchableDropdown
name="version"
label="Manage Users with access to a Version"
placeHolder="Search for a Version"
options={options.versions}
strictSearch={true}
onUserInput={this.setFormData}
value={this.state.version}
/>
{this.state.version &&
<StaticElement
label={'Users'}
text={Object.values(this.state.originalData).map((user) => {
if (user.versions.includes(this.state.version)) {
return <div>
<CheckboxElement
name={'usersByVersion'}
label={user.name}
value={
data[user.id].versions.includes(this.state.version)
}
onUserInput={(_, checked) =>
this.setFormData(
'usersByVersion',
{
userId: user.id,
checked,
version: this.state.version,
}
)
}
/><br/>
</div>;
}
}
)}
/>
}
</FormElement>
</Modal>
);
Expand All @@ -116,19 +165,25 @@ class ManagePermissionsForm extends Component {
/**
* Store the value of the element in this.state.data
*
* @param {string} userId
* @param {string} version
* @param {boolean} permission
* @param {string} formElement - name of the selected element
* @param {string} value - selected value for corresponding form element
*/
setFormData(userId, version, permission) {
setFormData(formElement, value) {
let {data} = JSON.parse(JSON.stringify(this.state));
if (permission) {
data[userId].versions = [...data[userId].versions, version];
if (formElement === 'versionsByUser' || formElement === 'usersByVersion') {
let {checked, version, userId} = value;
if (checked) {
data[userId].versions = [...data[userId].versions, version];
} else {
data[userId].versions = data[userId].versions
.filter((e) => e !== version);
}
this.setState({data});
} else {
data[userId].versions = data[userId].versions
.filter((e) => e !== version);
this.setState({[formElement]: (value === '' ? null : value)});
if (formElement != 'user') this.setState({user: null});
if (formElement != 'version') this.setState({version: null});
}
this.setState({data});
}

/**
Expand Down
17 changes: 17 additions & 0 deletions modules/data_release/jsx/uploadFileForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
FileElement,
TextboxElement,
ButtonElement,
SelectElement,
} from 'jsx/Form';

/**
Expand Down Expand Up @@ -79,6 +80,14 @@ class UploadFileForm extends Component {
required={false}
value={this.state.formData.version}
/>
<SelectElement
name='project'
label='Project'
onUserInput={this.updateFormElement}
required={true}
value={this.state.formData.project}
options={this.props.projects}
/>
<ButtonElement label='Upload File'/>
<div className='row'>
<div className='col-sm-9 col-sm-offset-3'>
Expand Down Expand Up @@ -130,6 +139,13 @@ class UploadFileForm extends Component {
return;
}

if (!formData.project) {
errorMessage.Project = 'You must select a project';
hasError.Project = true;
this.setState({errorMessage, hasError});
return;
}

// Check that the size of the file is not bigger than the allowed size
let fileSize = formData.file ? Math.round((formData.file.size/1024)) : null;
const maxSizeAllowed = this.state.data.maxUploadSize;
Expand Down Expand Up @@ -230,6 +246,7 @@ class UploadFileForm extends Component {
UploadFileForm.propTypes = {
DataURL: PropTypes.string.isRequired,
action: PropTypes.string.isRequired,
projects: PropTypes.array.isRequired,
};

export default UploadFileForm;
44 changes: 32 additions & 12 deletions modules/data_release/php/data_release.class.inc
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ class Data_Release extends \DataFrameworkMenu
function getUsersList(\Database $DB)
{
return $DB->pselectColWithIndexKey(
"SELECT ID, UserID FROM users",
[],
"SELECT DISTINCT ID, LOWER(users.UserID) as UserID FROM users
JOIN user_project_rel upr ON upr.UserID = users.ID
WHERE upr.ProjectID IN
(SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)
ORDER BY LOWER(users.UserID)",
[':userID' => \User::singleton()->getID()],
"ID"
);
}
Expand All @@ -75,8 +79,10 @@ class Data_Release extends \DataFrameworkMenu
function getFilesList(\Database $DB)
{
$result = $DB->pselectWithIndexKey(
"SELECT id, file_name, version FROM data_release",
[],
"SELECT id, file_name, version, ProjectID FROM data_release
WHERE ProjectID IS NULL OR ProjectID IN
(SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
[':userID' => \User::singleton()->getID()],
"id"
);

Expand All @@ -99,9 +105,12 @@ class Data_Release extends \DataFrameworkMenu
*/
function getVersionsList(\Database $DB)
{
$user =& \User::singleton();
$versions = $DB->pselectCol(
"SELECT DISTINCT version FROM data_release",
[],
"SELECT DISTINCT version FROM data_release
WHERE ProjectID IS NULL OR ProjectID IN
(SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
['userID' => $user->getID()],
"version"
);

Expand All @@ -125,8 +134,10 @@ class Data_Release extends \DataFrameworkMenu
function getVersionFiles(\Database $db)
{
$result = $db->pselect(
"SELECT version, id FROM data_release",
[]
"SELECT version, id FROM data_release
WHERE ProjectID IS NULL OR ProjectID IN
(SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
[':userID' => \User::singleton()->getID()]
);

$versionFiles = [];
Expand All @@ -151,16 +162,23 @@ class Data_Release extends \DataFrameworkMenu
$result = $db->pselect(
"SELECT u.ID as userId,
u.UserID as userName,
drp.data_release_id fileId
drp.data_release_id fileId,
dr.ProjectID as ProjectID
FROM users u
LEFT JOIN data_release_permissions drp ON (u.ID=drp.userid)
LEFT JOIN data_release dr ON (drp.data_release_id=dr.id)",
[]
LEFT JOIN data_release dr ON (drp.data_release_id=dr.id)
JOIN user_project_rel upr ON upr.UserID = u.ID
WHERE upr.ProjectID IN
(SELECT ProjectID FROM user_project_rel WHERE UserID = :userID)",
[':userID' => \User::singleton()->getID()]
);

error_log(print_r($result, true));

$userFiles = [];
foreach ($result as $row) {
$userFiles[$row['userId']]['name'] = $row['userName'];
$userFiles[$row['userId']]['id'] = $row['userId'];
if (empty($userFiles[$row['userId']]['files'])) {
$userFiles[$row['userId']]['files'] = [];
}
Expand Down Expand Up @@ -206,11 +224,13 @@ class Data_Release extends \DataFrameworkMenu
*/
protected function getFieldOptions() : array
{
$db = $this->loris->getDatabaseConnection();
$db = $this->loris->getDatabaseConnection();
$projects = \Utility::getProjectList();
return [
'users' => $this->getUsersList($db),
'versions' => $this->getVersionsList($db),
'filenames' => $this->getFilesList($db),
'projects' => array_combine($projects, $projects)
];
}

Expand Down
10 changes: 8 additions & 2 deletions modules/data_release/php/datareleaseprovisioner.class.inc
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ class DataReleaseProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner
file_name AS fileName,
IF(version is null or version ='','Unversioned', version) AS version,
upload_date AS uploadDate,
p.Name as ProjectName,
dr.id as dataReleaseID
FROM data_release dr";
FROM data_release dr
LEFT JOIN Project p on p.ProjectID = dr.ProjectID";

if (!$user->hasPermission("superuser")) {
$query .= "
Expand All @@ -54,7 +56,11 @@ class DataReleaseProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner
ON
(dr.id=drp.data_release_id)
WHERE
drp.UserID=".$user->getID();
drp.UserID=".$user->getID()." AND (
dr.ProjectID IS NULL OR dr.ProjectID IN
(SELECT ProjectID FROM user_project_rel
WHERE UserID = ".$user->getID().")
)";
}

$query .= " ORDER BY uploadDate";
Expand Down
Loading

0 comments on commit 38266d4

Please sign in to comment.