diff --git a/API.md b/API.md index b33ca6c0..f30a7d41 100644 --- a/API.md +++ b/API.md @@ -163,7 +163,7 @@ URL: `\/api/rest/v1/force\_run?\_format=json` ``` URL: `\/api/rest/v1/login?\_format=json` -- Use this to test your authorization data +- Use this to test your authorization data, at success it will return some basic account info - The following headers are needed for the request to be accepted: - Content-Type: application/json - Authorization: diff --git a/config/prod/system.action.user_add_role_action.qas_sub.yml b/config/prod/system.action.user_add_role_action.qas_sub.yml new file mode 100644 index 00000000..1f23907c --- /dev/null +++ b/config/prod/system.action.user_add_role_action.qas_sub.yml @@ -0,0 +1,14 @@ +uuid: 965a166e-d258-419c-a2e0-97ccf48d98c3 +langcode: en +status: true +dependencies: + config: + - user.role.qas_sub + module: + - user +id: user_add_role_action.qas_sub +label: 'Add the Subscriber role to the selected users' +type: user +plugin: user_add_role_action +configuration: + rid: qas_sub diff --git a/config/prod/system.action.user_remove_role_action.qas_sub.yml b/config/prod/system.action.user_remove_role_action.qas_sub.yml new file mode 100644 index 00000000..10680803 --- /dev/null +++ b/config/prod/system.action.user_remove_role_action.qas_sub.yml @@ -0,0 +1,14 @@ +uuid: ffe7f1ce-1042-4d9c-a738-feab3af06836 +langcode: en +status: true +dependencies: + config: + - user.role.qas_sub + module: + - user +id: user_remove_role_action.qas_sub +label: 'Remove the Subscriber role from the selected users' +type: user +plugin: user_remove_role_action +configuration: + rid: qas_sub diff --git a/config/prod/user.role.qas_sub.yml b/config/prod/user.role.qas_sub.yml new file mode 100644 index 00000000..d5a25fb1 --- /dev/null +++ b/config/prod/user.role.qas_sub.yml @@ -0,0 +1,27 @@ +uuid: 848c223c-401d-405b-baf4-621406655d88 +langcode: en +status: true +dependencies: { } +id: qas_sub +label: Subscriber +weight: -6 +is_admin: null +permissions: + - 'access qashot test overview' + - 'access qashot test overview via rest' + - 'add qashot test entities' + - 'add qashot test entities via rest' + - 'delete own qashot test entities' + - 'delete qashot test entities via rest' + - 'edit own qashot test entities' + - 'edit qashot test entities via rest' + - 'restful delete entity:qa_shot_test' + - 'restful get entity:qa_shot_test' + - 'restful patch entity:qa_shot_test' + - 'restful post entity:qa_shot_test' + - 'run own qashot test entities' + - 'view own published qashot test entities' + - 'view own unpublished content' + - 'view own unpublished qashot test entities' + - 'view published qashot test entities via rest' + - 'view unpublished qashot test entities via rest' diff --git a/config/prod/user.role.rest_api_user.yml b/config/prod/user.role.rest_api_user.yml index 31bf23f0..7566b907 100644 --- a/config/prod/user.role.rest_api_user.yml +++ b/config/prod/user.role.rest_api_user.yml @@ -14,12 +14,16 @@ permissions: - 'add qashot test entities via rest' - 'create qashot_test content' - 'delete all revisions' + - 'delete any qashot test entities' - 'delete any qashot_test content' + - 'delete own qashot test entities' - 'delete own qashot_test content' - 'delete qashot test entities' - 'delete qashot test entities via rest' - 'delete qashot_test revisions' + - 'edit any qashot test entities' - 'edit any qashot_test content' + - 'edit own qashot test entities' - 'edit own qashot_test content' - 'edit qashot test entities' - 'edit qashot test entities via rest' @@ -33,10 +37,17 @@ permissions: - 'restful post entity:qa_shot_test:node' - 'revert all revisions' - 'revert qashot_test revisions' + - 'run any qashot test entities' + - 'run own qashot test entities' - 'view all revisions' + - 'view any published qashot test entities' + - 'view any unpublished qashot test entities' + - 'view own published qashot test entities' - 'view own unpublished content' + - 'view own unpublished qashot test entities' - 'view published qashot test entities' - 'view published qashot test entities via rest' + - 'view qa_shot debug data' - 'view qashot_test revisions' - 'view unpublished qashot test entities' - 'view unpublished qashot test entities via rest' diff --git a/web/modules/custom/qa_shot/config/schema/qa_shot_test.views.schema.yml b/web/modules/custom/qa_shot/config/schema/qa_shot_test.views.schema.yml new file mode 100644 index 00000000..b014fb22 --- /dev/null +++ b/web/modules/custom/qa_shot/config/schema/qa_shot_test.views.schema.yml @@ -0,0 +1,17 @@ +# Schema for the views plugins of the QAShot module. + +views.filter.qa_shot_test_access: + type: views_filter + label: 'QAShot test access' + +views.filter.qa_shot_test_status: + type: views_filter + label: 'QAShot test status' + +views.filter_value.qa_shot_test_access: + type: string + label: 'Access' + +views.filter_value.qa_shot_test_status: + type: boolean + label: 'Status' diff --git a/web/modules/custom/qa_shot/qa_shot.api.php b/web/modules/custom/qa_shot/qa_shot.api.php new file mode 100644 index 00000000..248a64d5 --- /dev/null +++ b/web/modules/custom/qa_shot/qa_shot.api.php @@ -0,0 +1,367 @@ + 0, + * 'gid' => 888, + * 'realm' => 'example_realm', + * 'grant_view' => 1, + * 'grant_update' => 0, + * 'grant_delete' => 0, + * ); + * db_insert('qa_shot_test_access')->fields($record)->execute(); + * @endcode + * And then in its hook_qa_shot_test_grants() implementation, it would need to + * return: + * @code + * if ($op == 'view') { + * $grants['example_realm'] = array(888); + * } + * @endcode + * If you decide to do this, be aware that the qa_shot_test_access_rebuild() + * function will erase any qa_shot_test ID 0 entry when it is called, so you + * will need to make sure to restore your {qa_shot_test_access} record after + * qa_shot_test_access_rebuild() is called. + * + * For a detailed example, see qa_shot_test_access_example.module. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The account object whose grants are requested. + * @param string $op + * The qa_shot_test operation to be performed, such as 'view', 'update', or + * 'delete'. + * + * @return array + * An array whose keys are "realms" of grants, and whose values are arrays of + * the grant IDs within this realm that this user is being granted. + * + * @see qa_shot_test_access_view_all_qa_shot_tests() + * @see qa_shot_test_access_rebuild() + * @ingroup qa_shot_test_access + */ +function hook_qa_shot_test_grants(\Drupal\Core\Session\AccountInterface $account, $op) { + $grants = NULL; + if ($account->hasPermission('access private content')) { + $grants['example'] = [1]; + } + if ($account->id()) { + $grants['example_author'] = [$account->id()]; + } + return $grants; +} + +/** + * Set permissions for a qa_shot_test to be written to the database. + * + * When a qa_shot_test is saved, a module implementing + * hook_qa_shot_test_access_records() will be asked if it is interested in the + * access permissions for a qa_shot_test. If it is interested, it must respond + * with an array of permissions arrays for that qa_shot_test. + * + * qa_shot_test access grants apply regardless of the published or unpublished + * status of the qa_shot_test. Implementations must make sure not to grant + * access to unpublished qa_shot_tests if they don't want to change the standard + * access control behavior. Your module may need to create a separate access + * realm to handle access to unpublished qa_shot_tests. + * + * Note that the grant values in the return value from your hook must be + * integers and not boolean TRUE and FALSE. + * + * Each permissions item in the array is an array with the following elements: + * - 'realm': The name of a realm that the module has defined in + * hook_qa_shot_test_grants(). + * - 'gid': A 'grant ID' from hook_qa_shot_test_grants(). + * - 'grant_view': If set to 1 a user that has been identified as a member + * of this gid within this realm can view this qa_shot_test. This should + * usually be set to $qa_shot_test->isPublished(). Failure to do so may expose + * unpublished content to some users. + * - 'grant_update': If set to 1 a user that has been identified as a member + * of this gid within this realm can edit this qa_shot_test. + * - 'grant_delete': If set to 1 a user that has been identified as a member + * of this gid within this realm can delete this qa_shot_test. + * - langcode: (optional) The language code of a specific translation of the + * qa_shot_test, if any. Modules may add this key to grant different access to + * different translations of a qa_shot_test, such that (e.g.) a particular + * group is granted access to edit the Catalan version of the qa_shot_test, + * but not the Hungarian version. If no value is provided, the langcode is set + * automatically from the $qa_shot_test parameter and the qa_shot_test's + * original language (if specified) is used as a fallback. Only specify + * multiple grant records with different languages for a qa_shot_test if the + * site has those languages configured. + * + * A "deny all" grant may be used to deny all access to a particular + * qa_shot_test or qa_shot_test translation: + * @code + * $grants[] = array( + * 'realm' => 'all', + * 'gid' => 0, + * 'grant_view' => 0, + * 'grant_update' => 0, + * 'grant_delete' => 0, + * 'langcode' => 'ca', + * ); + * @endcode + * Note that another module qa_shot_test access module could override this by + * granting access to one or more qa_shot_tests, since grants are additive. To + * enforce that access is denied in a particular case, use + * hook_qa_shot_test_access_records_alter(). + * Also note that a deny all is not written to the database; denies are + * implicit. + * + * @param \Drupal\qa_shot\Entity\QAShotTestInterface $qa_shot_test + * The qa_shot_test that has just been saved. + * + * @return array + * An array of grants as defined above. + * + * @see hook_qa_shot_test_access_records_alter() + * @ingroup qa_shot_test_access + */ +function hook_qa_shot_test_access_records(\Drupal\qa_shot\Entity\QAShotTestInterface $qa_shot_test) { + // We only care about the qa_shot_test if it has been marked private. If not, + // it is treated just like any other qa_shot_test and we completely ignore it. + if ($qa_shot_test->private->value) { + $grants = []; + // Only published Catalan translations of private qa_shot_tests should be + // viewable to all users. If we fail to check $qa_shot_test->isPublished(), + // all users would be able to view an unpublished qa_shot_test. + if ($qa_shot_test->isPublished()) { + $grants[] = [ + 'realm' => 'example', + 'gid' => 1, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + 'langcode' => 'ca', + ]; + } + // For the example_author array, the GID is equivalent to a UID, which + // means there are many groups of just 1 user. + // Note that an author can always view his or her qa_shot_tests, even if + // they have status unpublished. + if ($qa_shot_test->getOwnerId()) { + $grants[] = [ + 'realm' => 'example_author', + 'gid' => $qa_shot_test->getOwnerId(), + 'grant_view' => 1, + 'grant_update' => 1, + 'grant_delete' => 1, + 'langcode' => 'ca', + ]; + } + + return $grants; + } +} + +/** + * Alter permissions for a qa_shot_test before it is written to the database. + * + * The qa_shot_test access modules establish rules for user access to content. + * qa_shot_test access records are stored in the {qa_shot_test_access} table and + * define which permissions are required to access a qa_shot_test. This hook is + * invoked after qa_shot_test access modules returned their requirements via + * hook_qa_shot_test_access_records(); doing so allows modules to modify the + * $grants array by reference before it is stored, so custom or advanced + * business logic can be applied. + * + * Upon viewing, editing or deleting a qa_shot_test, hook_qa_shot_test_grants() + * builds a permissions array that is compared against the stored access + * records. The user must have one or more matching permissions in order to + * complete the requested operation. + * + * A module may deny all access to a qa_shot_test by setting $grants to an empty + * array. + * + * The preferred use of this hook is in a module that bridges multiple + * qa_shot_test access modules with a configurable behavior, as shown in the + * example with the 'is_preview' field. + * + * @param array $grants + * The $grants array returned by hook_qa_shot_test_access_records(). + * @param \Drupal\qa_shot\Entity\QAShotTestInterface $qa_shot_test + * The qa_shot_test for which the grants were acquired. + * + * @see hook_qa_shot_test_access_records() + * @see hook_qa_shot_test_grants() + * @see hook_qa_shot_test_grants_alter() + * @ingroup qa_shot_test_access + */ +function hook_qa_shot_test_access_records_alter(array &$grants, \Drupal\qa_shot\Entity\QAShotTestInterface $qa_shot_test) { + // Our module allows editors to mark specific articles with the 'is_preview' + // field. If the qa_shot_test being saved has a TRUE value for that field, + // then only our grants are retained, and other grants are removed. Doing so + // ensures that our rules are enforced no matter what priority other grants + // are given. + if ($qa_shot_test->is_preview) { + // Our module grants are set in $grants['example']. + $temp = $grants['example']; + // Now remove all module grants but our own. + $grants = ['example' => $temp]; + } +} + +/** + * Alter user access rules when trying to view, edit or delete a qa_shot_test. + * + * The qa_shot_test access modules establish rules for user access to content. + * hook_qa_shot_test_grants() defines permissions for a user to view, edit or + * delete qa_shot_tests by building a $grants array that indicates the + * permissions assigned to the user by each qa_shot_test access module. This + * hook is called to allow modules to modify the $grants array by reference, so + * the interaction of multiple qa_shot_test access modules can be altered or + * advanced business logic can be applied. + * + * The resulting grants are then checked against the records stored in the + * {qa_shot_test_access} table to determine if the operation may be completed. + * + * A module may deny all access to a user by setting $grants to an empty array. + * + * Developers may use this hook to either add additional grants to a user or to + * remove existing grants. These rules are typically based on either the + * permissions assigned to a user role, or specific attributes of a user + * account. + * + * @param array $grants + * The $grants array returned by hook_qa_shot_test_grants(). + * @param \Drupal\Core\Session\AccountInterface $account + * The account requesting access to content. + * @param string $op + * The operation being performed, 'view', 'update' or 'delete'. + * + * @see hook_qa_shot_test_grants() + * @see hook_qa_shot_test_access_records() + * @see hook_qa_shot_test_access_records_alter() + * @ingroup qa_shot_test_access + */ +function hook_qa_shot_test_grants_alter(array &$grants, \Drupal\Core\Session\AccountInterface $account, $op) { + /* Our sample module never allows certain roles to edit or delete + * content. Since some other qa_shot_test access modules might allow this + * permission, we expressly remove it by returning an empty $grants + * array for roles specified in our variable setting. */ + + // Get our list of banned roles. + $restricted = \Drupal::config('example.settings')->get('restricted_roles'); + + if ($op != 'view' && !empty($restricted)) { + // Now check the roles for this account against the restrictions. + foreach ($account->getRoles() as $rid) { + if (in_array($rid, $restricted)) { + $grants = []; + } + } + } +} + +/** + * Controls access to a qa_shot_test. + * + * Modules may implement this hook if they want to have a say in whether or not + * a given user has access to perform a given operation on a qa_shot_test. + * + * The administrative account (user ID #1) always passes any access check, so + * this hook is not called in that case. Users with the "bypass qa_shot_test + * access" permission may always view and edit content through the + * administrative interface. + * + * Note that not all modules will want to influence access on all qa_shot_test + * types. If your module does not want to explicitly allow or forbid access, + * return an AccessResultInterface object with neither isAllowed() nor + * isForbidden() equaling TRUE. Blindly returning an object with isForbidden() + * equaling TRUE will break other qa_shot_test access modules. + * + * Also note that this function isn't called for qa_shot_test listings (e.g., + * RSS feeds, the default home page at path 'qa_shot_test', a recent content + * block, etc.) See + * @link qa_shot_test_access qa_shot_test access rights @endlink for a full explanation. + * + * @param \Drupal\qa_shot\Entity\QAShotTestInterface|string $qa_shot_test + * Either a qa_shot_test entity or the machine name of the content type on + * which to perform the access check. + * @param string $op + * The operation to be performed. Possible values: + * - "create" + * - "delete" + * - "update" + * - "view". + * @param \Drupal\Core\Session\AccountInterface $account + * The user object to perform the access check operation on. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + * + * @ingroup qa_shot_test_access + */ +function hook_qa_shot_test_access(\Drupal\qa_shot\Entity\QAShotTestInterface $qa_shot_test, $op, \Drupal\Core\Session\AccountInterface $account) { + $type = $qa_shot_test->bundle(); + + switch ($op) { + case 'create': + return AccessResult::allowedIfHasPermission($account, 'create ' . $type . ' content'); + + case 'update': + if ($account->hasPermission('edit any qashot test entities')) { + return AccessResult::allowed()->cachePerPermissions(); + } + else { + return AccessResult::allowedIf($account->hasPermission('edit own ' . $type . ' content') && ($account->id() == $qa_shot_test->getOwnerId()))->cachePerPermissions()->cachePerUser()->addCacheableDependency($qa_shot_test); + } + + case 'delete': + if ($account->hasPermission('delete any qashot test entities')) { + return AccessResult::allowed()->cachePerPermissions(); + } + else { + return AccessResult::allowedIf($account->hasPermission('delete own ' . $type . ' content') && ($account->id() == $qa_shot_test->getOwnerId()))->cachePerPermissions()->cachePerUser()->addCacheableDependency($qa_shot_test); + } + + default: + // No opinion. + return AccessResult::neutral(); + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/web/modules/custom/qa_shot/qa_shot.install b/web/modules/custom/qa_shot/qa_shot.install index 10ce858c..22c5b75c 100644 --- a/web/modules/custom/qa_shot/qa_shot.install +++ b/web/modules/custom/qa_shot/qa_shot.install @@ -13,7 +13,7 @@ use Drupal\system\Entity\Action; /** * Implements hook_requirements(). */ -function qa_shot_requirements() { +function qa_shot_requirements($phase) { $requirements = []; $requirements['qa_shot_required_php_version'] = [ @@ -27,6 +27,27 @@ function qa_shot_requirements() { $requirements['qa_shot_required_php_version']['severity'] = REQUIREMENT_ERROR; } + if ($phase === 'runtime') { + // Only show rebuild button if there are either 0, or 2 or more, rows + // in the {qa_shot_test_access} table, or if there are modules that + // implement hook_qa_shot_test_grants(). + $grant_count = \Drupal::entityManager()->getAccessControlHandler('qa_shot_test')->countGrants(); + if ($grant_count != 1 || count(\Drupal::moduleHandler()->getImplementations('qa_shot_test_grants')) > 0) { + $value = \Drupal::translation()->formatPlural($grant_count, 'One permission in use', '@count permissions in use', ['@count' => $grant_count]); + } + else { + $value = t('Disabled'); + } + + $requirements['qa_shot_test_access'] = [ + 'title' => t('QAShot Test Access Permissions'), + 'value' => $value, + 'description' => t('If the site is experiencing problems with permissions to content, you may have to rebuild the permissions cache. Rebuilding will remove all privileges to content and replace them with permissions based on the current modules and settings. Rebuilding may take some time if there is a lot of content or complex permission settings. After rebuilding has completed, content will automatically use the new permissions. Rebuild permissions', [ + ':rebuild' => \Drupal::url('qa_shot.configure_rebuild_confirm'), + ]), + ]; + } + return $requirements; } @@ -39,6 +60,23 @@ function qa_shot_schema() { return $schemaDefinitions->everySchema(); } +/** + * Implements hook_install(). + */ +function qa_shot_install() { + // Populate the QAShot test access table. + db_insert('qa_shot_test_access') + ->fields([ + 'nid' => 0, + 'gid' => 0, + 'realm' => 'all', + 'grant_view' => 0, + 'grant_update' => 0, + 'grant_delete' => 0, + ]) + ->execute(); +} + /** * Remove every QAShot Test entity. */ diff --git a/web/modules/custom/qa_shot/qa_shot.module b/web/modules/custom/qa_shot/qa_shot.module index 94c22dfd..5b91a1e7 100644 --- a/web/modules/custom/qa_shot/qa_shot.module +++ b/web/modules/custom/qa_shot/qa_shot.module @@ -5,8 +5,14 @@ * Contains qa_shot.module. */ +use Drupal\Core\Database\Query\AlterableInterface; +use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\qa_shot\Entity\QAShotTest; +use Drupal\qa_shot\Entity\QAShotTestInterface; +use Drupal\user\Entity\User; /** * Implements hook_help(). @@ -201,3 +207,433 @@ function qa_shot_qa_shot_test_run_handler(&$form, FormStateInterface $form_state ); } } + +/** + * Fetches an array of permission IDs granted to the given user ID. + * + * The implementation here provides only the universal "all" grant. A + * qa_shot_test access module should implement hook_qa_shot_test_grants() to + * provide a grant list for the user. + * + * After the default grants have been loaded, we allow modules to alter the + * grants array by reference. This hook allows for complex business logic to be + * applied when integrating multiple qa_shot_test access modules. + * + * @param string $op + * The operation that the user is trying to perform. + * @param \Drupal\Core\Session\AccountInterface $account + * The account object for the user performing the operation. + * + * @return array + * An associative array in which the keys are realms, and the values are + * arrays of grants for those realms. + */ +function qa_shot_access_grants($op, AccountInterface $account) { + // Fetch qa_shot_test access grants from other modules. + $grants = \Drupal::moduleHandler()->invokeAll('qa_shot_test_grants', [$account, $op]); + // Allow modules to alter the assigned grants. + \Drupal::moduleHandler()->alter('qa_shot_test_grants', $grants, $account, $op); + + return array_merge(['all' => [0]], $grants); +} + +/** + * Determines whether the user has a global viewing grant for all qa_shot_tests. + * + * Checks to see whether any module grants global 'view' access to a user + * account; global 'view' access is encoded in the {qa_shot_test_access} table + * as a grant with id=0. If no qa_shot_test access modules are enabled, + * qa_shot_test.module defines such a global 'view' access grant. + * + * This function is called when a qa_shot_test listing query is tagged with + * 'qa_shot_test_access'; when this function returns TRUE, no qa_shot_test + * access joins are added to the query. + * + * @param \Drupal\Core\Session\AccountInterface $account + * (optional) The user object for the user whose access is being checked. If + * omitted, the current user is used. Defaults to NULL. + * + * @return array + * TRUE if 'view' access to all qa_shot_tests is granted, FALSE otherwise. + * + * @see hook_qa_shot_test_grants() + * @see qa_shot_query_qa_shot_test_access_alter() + */ +function qa_shot_access_view_all_qa_shot_tests(AccountInterface $account = NULL) { + + if (!$account) { + $account = \Drupal::currentUser(); + } + + // Statically cache results in an array keyed by $account->id(). + $access = &drupal_static(__FUNCTION__); + if (isset($access[$account->id()])) { + return $access[$account->id()]; + } + + // If no modules implement the qa_shot_test access system, access is always + // TRUE. + if (!\Drupal::moduleHandler()->getImplementations('qa_shot_test_grants')) { + $access[$account->id()] = TRUE; + } + else { + $access[$account->id()] = \Drupal::entityManager()->getAccessControlHandler('qa_shot_test')->checkAllGrants($account); + } + + return $access[$account->id()]; +} + +/** + * Implements hook_query_TAG_alter(). + * + * This is the hook_query_alter() for queries tagged with 'qa_shot_test_access'. + * It adds qa_shot_test access checks for the user account given by the + * 'account' meta-data (or current user if not provided), for an operation given + * by the 'op' meta-data (or 'view' if not provided; other possible values are + * 'update' and 'delete'). + * + * Queries tagged with 'qa_shot_test_access' that are not against the + * {qa_shot_test} table must add the base table as metadata. For example: + * @code + * $query + * ->addTag('qa_shot_test_access') + * ->addMetaData('base_table', 'taxonomy_index'); + * @endcode + */ +function qa_shot_query_qa_shot_test_access_alter(AlterableInterface $query) { + // Read meta-data from query, if provided. + if (!$account = $query->getMetaData('account')) { + $account = \Drupal::currentUser(); + } + if (!$op = $query->getMetaData('op')) { + $op = 'view'; + } + + // If $account can bypass qa_shot_test access, or there are no qa_shot_test + // access modules, or the operation is 'view' and the $account has a global + // view grant (such as a view grant for qa_shot_test ID 0), we don't need to + // alter the query. + if ($account->hasPermission('bypass qashot access')) { + return; + } + if (!count(\Drupal::moduleHandler()->getImplementations('qa_shot_test_grants'))) { + return; + } + if ($op == 'view' && qa_shot_access_view_all_qa_shot_tests($account)) { + return; + } + + $tables = $query->getTables(); + $base_table = $query->getMetaData('base_table'); + // If the base table is not given, default to one of the qa_shot_test base + // tables. + if (!$base_table) { + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = \Drupal::entityTypeManager()->getStorage('qa_shot_test')->getTableMapping(); + $qa_shot_test_base_tables = $table_mapping->getTableNames(); + + foreach ($tables as $table_info) { + if (!($table_info instanceof SelectInterface)) { + $table = $table_info['table']; + // Ensure that 'qa_shot_test' and 'qa_shot_test_field_data' are always + // preferred over 'qa_shot_test_revision' and + // 'qa_shot_test_field_revision'. + if ($table == 'qa_shot_test' || $table == 'qa_shot_test_field_data') { + $base_table = $table; + break; + } + // If one of the qa_shot_test base tables are in the query, add it to + // the list of possible base tables to join against. + if (in_array($table, $qa_shot_test_base_tables)) { + $base_table = $table; + } + } + } + + // Bail out if the base table is missing. + if (!$base_table) { + throw new Exception(t('Query tagged for qa_shot_test access but there is no qa_shot_test table, specify the base_table using meta data.')); + } + } + + // Update the query for the given storage method. + \Drupal::service('qa_shot.grant_storage')->alterQuery($query, $tables, $op, $account, $base_table); + + // Bubble the 'user.qa_shot_test_grants:$op' cache context to the current + // render context. + $request = \Drupal::requestStack()->getCurrentRequest(); + $renderer = \Drupal::service('renderer'); + if ($request->isMethodSafe() && $renderer->hasRenderContext()) { + $build = ['#cache' => ['contexts' => ['user.qa_shot_test_grants:' . $op]]]; + $renderer->render($build); + } +} + +/** + * Toggles or reads the value of a flag for rebuilding the QAShot access grants. + * + * When the flag is set, a message is displayed to users with 'access + * administration pages' permission, pointing to the 'rebuild' confirm form. + * This can be used as an alternative to direct qa_shot_test_access_rebuild + * calls, allowing administrators to decide when they want to perform the actual + * (possibly time consuming) rebuild. + * + * When unsure if the current user is an administrator, + * qa_shot_test_access_rebuild() should be used instead. + * + * @param bool|null $rebuild + * (optional) The boolean value to be written. + * + * @return bool|null + * The current value of the flag if no value was provided for $rebuild. If a + * value was provided for $rebuild, nothing (NULL) is returned. + * + * @see qa_shot_test_access_rebuild() + */ +function qa_shot_access_needs_rebuild($rebuild = NULL) { + if (!isset($rebuild)) { + return \Drupal::state()->get('qa_shot_test.qa_shot_test_access_needs_rebuild') ?: FALSE; + } + elseif ($rebuild) { + \Drupal::state()->set('qa_shot_test.qa_shot_test_access_needs_rebuild', TRUE); + } + else { + \Drupal::state()->delete('qa_shot_test.qa_shot_test_access_needs_rebuild'); + } +} + +/** + * Rebuilds the qa_shot_test access database. + * + * This rebuild is occasionally needed by modules that make system-wide changes + * to access levels. When the rebuild is required by an admin-triggered action + * (e.g module settings form), calling qa_shot_test_access_needs_rebuild(TRUE) + * instead of qa_shot_test_access_rebuild() lets the user perform his changes + * and actually rebuild only once he is done. + * + * Note : As of Drupal 6, qa_shot_test access modules are not required to (and + * actually should not) call qa_shot_test_access_rebuild() in + * hook_install/uninstall anymore. + * + * @param bool $batch_mode + * (optional) Set to TRUE to process in 'batch' mode, spawning processing over + * several HTTP requests (thus avoiding the risk of PHP timeout if the site + * has a large number of qa_shot_tests). hook_update_N() and any form submit + * handler are safe contexts to use the 'batch mode'. Less decidable cases + * (such as calls from hook_user(), hook_taxonomy(), etc.) might consider + * using the non-batch mode. Defaults to FALSE. + * + * @see qa_shot_test_access_needs_rebuild() + */ +function qa_shot_access_rebuild($batch_mode = FALSE) { + $qa_shot_test_storage = \Drupal::entityManager()->getStorage('qa_shot_test'); + /** @var \Drupal\qa_shot\QAShotTestAccessControlHandlerInterface $access_control_handler */ + $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('qa_shot_test'); + $access_control_handler->deleteGrants(); + // Only recalculate if the site is using a qa_shot_test_access module. + if (count(\Drupal::moduleHandler()->getImplementations('qa_shot_test_grants'))) { + if ($batch_mode) { + $batch = [ + 'title' => t('Rebuilding content access permissions'), + 'operations' => [ + ['_qa_shot_access_rebuild_batch_operation', []], + ], + 'finished' => '_qa_shot_test_access_rebuild_batch_finished', + ]; + batch_set($batch); + } + else { + // Try to allocate enough time to rebuild qa_shot_test grants. + drupal_set_time_limit(240); + + // Rebuild newest qa_shot_tests first so that recent content becomes + // available quickly. + $entity_query = \Drupal::entityQuery('qa_shot_test'); + $entity_query->sort('id', 'DESC'); + // Disable access checking since all qa_shot_tests must be processed even + // if the user does not have access. And unless the current user has the + // bypass qa_shot_test access permission, no qa_shot_tests are accessible + // since the grants have just been deleted. + $entity_query->accessCheck(FALSE); + $ids = $entity_query->execute(); + foreach ($ids as $id) { + $qa_shot_test_storage->resetCache([$id]); + $qa_shot_test = QAShotTest::load($id); + // To preserve database integrity, only write grants if the qa_shot_test + // loads successfully. + if (!empty($qa_shot_test)) { + $grants = $access_control_handler->acquireGrants($qa_shot_test); + \Drupal::service('qa_shot.grant_storage')->write($qa_shot_test, $grants); + } + } + } + } + else { + // Not using any qa_shot_test_access modules. Add the default grant. + $access_control_handler->writeDefaultGrant(); + } + + if (!isset($batch)) { + drupal_set_message(t('Content permissions have been rebuilt.')); + qa_shot_access_needs_rebuild(FALSE); + } +} + +/** + * Implements callback_batch_operation(). + * + * Performs batch operation for qa_shot_test_access_rebuild(). + * + * This is a multistep operation: we go through all qa_shot_tests by packs of + * 20. The batch processing engine interrupts processing and sends progress + * feedback after 1 second execution time. + * + * @param array $context + * An array of contextual key/value information for rebuild batch process. + */ +function _qa_shot_access_rebuild_batch_operation(array &$context) { + $qa_shot_test_storage = \Drupal::entityManager()->getStorage('qa_shot_test'); + if (empty($context['sandbox'])) { + // Initiate multistep processing. + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_qa_shot_test'] = 0; + $context['sandbox']['max'] = \Drupal::entityQuery('qa_shot_test')->accessCheck(FALSE)->count()->execute(); + } + + // Process the next 20 qa_shot_tests. + $limit = 20; + $ids = \Drupal::entityQuery('qa_shot_test') + ->condition('id', $context['sandbox']['current_qa_shot_test'], '>') + ->sort('id', 'ASC') + // Disable access checking since all qa_shot_tests must be processed even if + // the user does not have access. And unless the current user has the bypass + // qa_shot_test access permission, no qa_shot_tests are accessible since the + // grants have just been deleted. + ->accessCheck(FALSE) + ->range(0, $limit) + ->execute(); + $qa_shot_test_storage->resetCache($ids); + $qa_shot_tests = QAShotTest::loadMultiple($ids); + foreach ($qa_shot_tests as $id => $qa_shot_test) { + // To preserve database integrity, only write grants if the qa_shot_test + // loads successfully. + if (!empty($qa_shot_test)) { + /** @var \Drupal\qa_shot\QAShotTestAccessControlHandlerInterface $access_control_handler */ + $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('qa_shot_test'); + $grants = $access_control_handler->acquireGrants($qa_shot_test); + \Drupal::service('qa_shot.grant_storage')->write($qa_shot_test, $grants); + } + $context['sandbox']['progress']++; + $context['sandbox']['current_qa_shot_test'] = $id; + } + + // Multistep processing : report progress. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } +} + +/** + * Implements callback_batch_finished(). + * + * Performs post-processing for qa_shot_test_access_rebuild(). + * + * @param bool $success + * A boolean indicating whether the re-build process has completed. + * @param array $results + * An array of results information. + * @param array $operations + * An array of function calls (not used in this function). + */ +function _qa_shot_access_rebuild_batch_finished($success, array $results, array $operations) { + if ($success) { + drupal_set_message(t('The content access permissions have been rebuilt.')); + qa_shot_access_needs_rebuild(FALSE); + } + else { + drupal_set_message(t('The content access permissions have not been properly rebuilt.'), 'error'); + } +} + +/** + * @} End of "defgroup qa_shot_test_access". + */ + +/** + * Implements hook_modules_installed(). + */ +function qa_shot_test_modules_installed($modules) { + // Check if any of the newly enabled modules require the qa_shot_test_access + // table to be rebuilt. + if (!qa_shot_access_needs_rebuild() && array_intersect($modules, \Drupal::moduleHandler()->getImplementations('qa_shot_test_grants'))) { + qa_shot_access_needs_rebuild(TRUE); + } +} + +/** + * Implements hook_modules_uninstalled(). + */ +function qa_shot_test_modules_uninstalled($modules) { + // Check whether any of the disabled modules implemented + // hook_qa_shot_test_grants(), in which case the qa_shot_test access table + // needs to be rebuilt. + foreach ($modules as $module) { + // At this point, the module is already disabled, but its code is still + // loaded in memory. Module functions must no longer be called. We only + // check whether a hook implementation function exists and do not invoke it. + // Node access also needs to be rebuilt if language module is disabled to + // remove any language-specific grants. + if (!qa_shot_access_needs_rebuild() && (\Drupal::moduleHandler()->implementsHook($module, 'qa_shot_test_grants') || $module == 'language')) { + qa_shot_access_needs_rebuild(TRUE); + } + } + + // If there remains no more qa_shot_test_access module, rebuilding will be + // straightforward, we can do it right now. + if (qa_shot_access_needs_rebuild() && count(\Drupal::moduleHandler()->getImplementations('qa_shot_test_grants')) == 0) { + qa_shot_access_rebuild(); + } +} + +/** + * Implements hook_qa_shot_test_access_records(). + */ +function qa_shot_qa_shot_test_access_records(QAShotTestInterface $qa_shot_test) { + $grants = []; + + $ids = \Drupal::entityQuery('user') + ->condition('status', 1) + ->execute(); + $users = User::loadMultiple($ids); + + foreach ($users as $user) { + /* @var $user \Drupal\user\Entity\User */ + $grants[] = [ + 'id' => $qa_shot_test->id(), + 'realm' => 'qa_shot_access', + 'gid' => $user->id(), + 'grant_view' => $qa_shot_test->access('view', $user), + 'grant_update' => $qa_shot_test->access('update', $user), + 'grant_delete' => $qa_shot_test->access('delete', $user), + ]; + } + + return $grants; +} + +/** + * Implements hook_qa_shot_test_grants(). + * + * Make this as simple as possible, because this will run always when there's + * need an access check. + */ +function qa_shot_qa_shot_test_grants(AccountInterface $account, $op) { + $grants = []; + + if ($account->id() != 0) { + // Otherwise return uid, might match entry in table. + $grants['qa_shot_access'][] = $account->id(); + } + + return $grants; +} diff --git a/web/modules/custom/qa_shot/qa_shot.permissions.yml b/web/modules/custom/qa_shot/qa_shot.permissions.yml index 4daf8664..12fc188c 100644 --- a/web/modules/custom/qa_shot/qa_shot.permissions.yml +++ b/web/modules/custom/qa_shot/qa_shot.permissions.yml @@ -16,20 +16,43 @@ administer qashot queue: description: 'Allow to access the administration form to configure the queue.' restrict access: true -delete qashot test entities: - title: 'Delete QAShot Test entities' +bypass qashot access: + title: 'Bypass QAShot Test entities access' + description: 'Allow to access any entities with any operation.' + restrict access: true + +delete own qashot test entities: + title: 'Delete own QAShot Test entities' + +delete any qashot test entities: + title: 'Delete any QAShot Test entities' + +edit own qashot test entities: + title: 'Edit own QAShot Test entities' -edit qashot test entities: - title: 'Edit QAShot Test entities' +edit any qashot test entities: + title: 'Edit any QAShot Test entities' + +run own qashot test entities: + title: 'Run own QAShot Test entities' + +run any qashot test entities: + title: 'Run any QAShot Test entities' access qashot test overview: title: 'Access the QAShot Test overview page' -view published qashot test entities: - title: 'View published QAShot Test entities' +view own published qashot test entities: + title: 'View own published QAShot Test entities' + +view any published qashot test entities: + title: 'View any published QAShot Test entities' + +view own unpublished qashot test entities: + title: 'View own unpublished QAShot Test entities' -view unpublished qashot test entities: - title: 'View unpublished QAShot Test entities' +view any unpublished qashot test entities: + title: 'View any unpublished QAShot Test entities' view qa_shot debug data: title: 'View QAShot debug data' diff --git a/web/modules/custom/qa_shot/qa_shot.routing.yml b/web/modules/custom/qa_shot/qa_shot.routing.yml index 02b4e6d6..4c285c9a 100644 --- a/web/modules/custom/qa_shot/qa_shot.routing.yml +++ b/web/modules/custom/qa_shot/qa_shot.routing.yml @@ -4,7 +4,7 @@ entity.qa_shot_test.run: _controller: '\Drupal\qa_shot\Controller\QAShotController::entityRunPage' _title: 'Status' requirements: - _permission: 'edit qashot test entities' + _custom_access: '\Drupal\qa_shot\QAShotTestRunAccessControl::checkRunAccess' entity.qa_shot_test.debug: path: 'qa_shot_test/{qa_shot_test}/{file_name}/debug' @@ -20,7 +20,7 @@ entity.qa_shot_test.add_to_queue: _controller: '\Drupal\qa_shot\Controller\QAShotController::entityAddToQueue' _title: 'Status' requirements: - _permission: 'edit qashot test entities' + _custom_access: '\Drupal\qa_shot\QAShotTestRunAccessControl::checkRunAccess' entity.qa_shot_test.add_to_queue_reference: path: 'qa_shot_test/{qa_shot_test}/add_queue/reference' @@ -29,7 +29,7 @@ entity.qa_shot_test.add_to_queue_reference: _title: 'Status' run_type: 'before' requirements: - _permission: 'edit qashot test entities' + _custom_access: '\Drupal\qa_shot\QAShotTestRunAccessControl::checkRunAccess' entity.qa_shot_test.add_to_queue_test: path: 'qa_shot_test/{qa_shot_test}/add_queue/test' @@ -38,7 +38,7 @@ entity.qa_shot_test.add_to_queue_test: _title: 'Status' run_type: 'after' requirements: - _permission: 'edit qashot test entities' + _custom_access: '\Drupal\qa_shot\QAShotTestRunAccessControl::checkRunAccess' qa_shot.settings: path: '/admin/config/qa_shot' @@ -63,3 +63,10 @@ qa_shot.queue_list: _title: 'QAShot Queue' requirements: _permission: 'administer qashot queue' + +qa_shot.configure_rebuild_confirm: + path: '/admin/reports/status/rebuild-qashot' + defaults: + _form: 'Drupal\qa_shot\Form\RebuildPermissionsForm' + requirements: + _permission: 'access administration pages' diff --git a/web/modules/custom/qa_shot/qa_shot.services.yml b/web/modules/custom/qa_shot/qa_shot.services.yml index 9e60cd3a..d3451783 100644 --- a/web/modules/custom/qa_shot/qa_shot.services.yml +++ b/web/modules/custom/qa_shot/qa_shot.services.yml @@ -34,3 +34,15 @@ services: qa_shot.immediately_test: class: Drupal\qa_shot\Service\RunTestImmediately arguments: ['@plugin.manager.qa_shot_queue_worker', '@qa_shot.test_queue_factory', '@entity_type.manager', '@logger.factory'] + + qa_shot.grant_storage: + class: Drupal\qa_shot\QAShotGrantDatabaseStorage + arguments: ['@database', '@module_handler', '@language_manager'] + tags: + - { name: backend_overridable } + + cache_context.user.qa_shot_test_grants: + class: Drupal\qa_shot\Cache\QAShotAccessGrantsCacheContext + arguments: ['@current_user'] + tags: + - { name: cache.context } diff --git a/web/modules/custom/qa_shot/qa_shot.views_execution.inc b/web/modules/custom/qa_shot/qa_shot.views_execution.inc new file mode 100644 index 00000000..19a0b64e --- /dev/null +++ b/web/modules/custom/qa_shot/qa_shot.views_execution.inc @@ -0,0 +1,65 @@ + intval($account->hasPermission('administer qashot test entities')), + '***VIEW_OWN_UNPUBLISHED_QASHOT_TESTS***' => intval($account->hasPermission('view own unpublished qashot test entities')), + '***BYPASS_QASHOT_TEST_ACCESS***' => intval($account->hasPermission('bypass qashot access')), + ]; +} + +/** + * Implements hook_views_analyze(). + */ +function qa_shot_views_analyze(ViewExecutable $view) { + $ret = []; + + // Check for something other than the default display: + if ($view->storage->get('base_table') == 'qa_shot_test') { + foreach ($view->displayHandlers as $display) { + if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) { + // Check for no access control. + $access = $display->getOption('access'); + if (empty($access['type']) || $access['type'] == 'none') { + $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID); + $anonymous_has_access = $anonymous_role && $anonymous_role->hasPermission('access content'); + $authenticated_role = Role::load(RoleInterface::AUTHENTICATED_ID); + $authenticated_has_access = $authenticated_role && $authenticated_role->hasPermission('access content'); + if (!$anonymous_has_access || !$authenticated_has_access) { + $ret[] = Analyzer::formatMessage(t('Some roles lack permission to access content, but display %display has no access control.', ['%display' => $display->display['display_title']]), 'warning'); + } + $filters = $display->getOption('filters'); + foreach ($filters as $filter) { + if ($filter['table'] == 'qa_shot_test' && ($filter['field'] == 'status' || $filter['field'] == 'status_extra')) { + continue 2; + } + } + $ret[] = Analyzer::formatMessage(t('Display %display has no access control but does not contain a filter for published QAShot tests.', ['%display' => $display->display['display_title']]), 'warning'); + } + } + } + } + foreach ($view->displayHandlers as $display) { + if ($display->getPluginId() == 'page') { + if ($display->getOption('path') == 'qa_shot_test/%') { + $ret[] = Analyzer::formatMessage(t('Display %display has set qa_shot_test/% as path. This will not produce what you want. If you want to have multiple versions of the QAShot test view, use panels.', ['%display' => $display->display['display_title']]), 'warning'); + } + } + } + + return $ret; +} diff --git a/web/modules/custom/qa_shot/src/Cache/QAShotAccessGrantsCacheContext.php b/web/modules/custom/qa_shot/src/Cache/QAShotAccessGrantsCacheContext.php new file mode 100644 index 00000000..d913cec2 --- /dev/null +++ b/web/modules/custom/qa_shot/src/Cache/QAShotAccessGrantsCacheContext.php @@ -0,0 +1,104 @@ +user->hasPermission('bypass qashot access')) { + return 'all'; + } + + // When no specific operation is specified, check the grants for all three + // possible operations. + if ($operation === NULL) { + $result = []; + foreach (['view', 'update', 'delete'] as $op) { + $result[] = $this->checkNodeGrants($op); + } + return implode('-', $result); + } + else { + return $this->checkNodeGrants($operation); + } + } + + /** + * Checks the qa_shot_test grants for the given operation. + * + * @param string $operation + * The operation to check the qa_shot_test grants for. + * + * @return string + * The string representation of the cache context. + */ + protected function checkNodeGrants($operation) { + // When checking the grants for the 'view' operation and the current user + // has a global view grant (i.e. a view grant for qa_shot_test ID 0) — note + // that this is automatically the case if no qa_shot_test access modules + // exist (no hook_qa_shot_test_grants() implementations) then we don't need + // to determine the exact qa_shot_test view grants for the current user. + if ($operation === 'view' && qa_shot_access_view_all_qa_shot_tests($this->user)) { + return 'view.all'; + } + + $grants = qa_shot_access_grants($operation, $this->user); + $grants_context_parts = []; + foreach ($grants as $realm => $gids) { + $grants_context_parts[] = $realm . ':' . implode(',', $gids); + } + return $operation . '.' . implode(';', $grants_context_parts); + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata($operation = NULL) { + $cacheable_metadata = new CacheableMetadata(); + + if (!\Drupal::moduleHandler()->getImplementations('qa_shot_test_grants')) { + return $cacheable_metadata; + } + + // The qa_shot_test grants may change if the user is updated. (The max-age + // is set to zero below, but sites may override this cache context, and + // change it to a non-zero value. In such cases, this cache tag is needed + // for correctness.) + $cacheable_metadata->setCacheTags(['user:' . $this->user->id()]); + + // If the site is using qa_shot_test grants, this cache context can not be + // optimized. + return $cacheable_metadata->setCacheMaxAge(0); + } + +} diff --git a/web/modules/custom/qa_shot/src/Entity/QAShotTest.php b/web/modules/custom/qa_shot/src/Entity/QAShotTest.php index 9c760f5b..044d9483 100644 --- a/web/modules/custom/qa_shot/src/Entity/QAShotTest.php +++ b/web/modules/custom/qa_shot/src/Entity/QAShotTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList; use Drupal\qa_shot\Exception\QAShotBaseException; use Drupal\qa_shot\Plugin\DataType\ComputedLastRunMetadata; @@ -69,11 +70,34 @@ class QAShotTest extends ContentEntityBase implements QAShotTestInterface { /** * {@inheritdoc} */ - public static function preCreate(EntityStorageInterface $entityStorage, array &$values) { - parent::preCreate($entityStorage, $values); - $values += [ - 'user_id' => \Drupal::currentUser()->id(), - ]; + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + foreach (array_keys($this->getTranslationLanguages()) as $langcode) { + $translation = $this->getTranslation($langcode); + + // If no owner has been set explicitly, make the anonymous user the owner. + if (!$translation->getOwner()) { + $translation->setOwnerId(0); + } + } + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + // Update the node access table for this node, but only if it is the + // default revision. There's no need to delete existing records if the node + // is new. + if ($this->isDefaultRevision()) { + /** @var \Drupal\qa_shot\QAShotTestAccessControlHandlerInterface $access_control_handler */ + $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('qa_shot_test'); + $grants = $access_control_handler->acquireGrants($this); + \Drupal::service('qa_shot.grant_storage')->write($this, $grants, NULL, $update); + } } /** @@ -82,6 +106,25 @@ public static function preCreate(EntityStorageInterface $entityStorage, array &$ public static function preDelete(EntityStorageInterface $storage, array $entities) { // @todo: don't allow if it's running. // TODO: Implement preDelete() method. + parent::preDelete($storage, $entities); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $nodes) { + parent::postDelete($storage, $nodes); + \Drupal::service('qa_shot.grant_storage')->deleteQAShotRecords(array_keys($nodes)); + } + + /** + * {@inheritdoc} + */ + public static function preCreate(EntityStorageInterface $entityStorage, array &$values) { + parent::preCreate($entityStorage, $values); + $values += [ + 'user_id' => \Drupal::currentUser()->id(), + ]; } /** @@ -97,6 +140,14 @@ public function delete() { parent::delete(); } + /** + * {@inheritdoc} + */ + public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) { + // This override exists to set the operation to the default value "view". + return parent::access($operation, $account, $return_as_object); + } + /** * {@inheritdoc} */ diff --git a/web/modules/custom/qa_shot/src/Entity/QAShotTestViewsData.php b/web/modules/custom/qa_shot/src/Entity/QAShotTestViewsData.php index 11414f58..cb21cfd5 100644 --- a/web/modules/custom/qa_shot/src/Entity/QAShotTestViewsData.php +++ b/web/modules/custom/qa_shot/src/Entity/QAShotTestViewsData.php @@ -15,6 +15,8 @@ class QAShotTestViewsData extends EntityViewsData { public function getViewsData(): array { $data = parent::getViewsData(); + $data['qa_shot_test']['table']['base']['access query tag'] = 'qa_shot_test_access'; + // Additional information for Views integration, such as table joins, can be // put here. $data['views']['table']['group'] = t('Custom Global'); @@ -47,6 +49,26 @@ public function getViewsData(): array { ], ]; + // Define the base group of this table. Fields that don't have a group + // defined will go into this field by default. + $data['qa_shot_test_access']['table']['group'] = $this->t('Content access'); + + // For other base tables, explain how we join. + /*$data['qa_shot_test_access']['table']['join'] = [ + 'qa_shot_test' => [ + 'left_field' => 'id', + 'field' => 'id', + ], + ];*/ + $data['qa_shot_test_access']['id'] = [ + 'title' => $this->t('Access'), + 'help' => $this->t('Filter by access.'), + 'filter' => [ + 'id' => 'qa_shot_test_access', + 'help' => $this->t('Filter for content by view access. Not necessary if you are using QAShot test as your base table.'), + ], + ]; + return $data; } diff --git a/web/modules/custom/qa_shot/src/Form/RebuildPermissionsForm.php b/web/modules/custom/qa_shot/src/Form/RebuildPermissionsForm.php new file mode 100644 index 00000000..f61f7655 --- /dev/null +++ b/web/modules/custom/qa_shot/src/Form/RebuildPermissionsForm.php @@ -0,0 +1,59 @@ +setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/custom/qa_shot/src/Plugin/views/filter/Access.php b/web/modules/custom/qa_shot/src/Plugin/views/filter/Access.php new file mode 100644 index 00000000..81975034 --- /dev/null +++ b/web/modules/custom/qa_shot/src/Plugin/views/filter/Access.php @@ -0,0 +1,68 @@ +view->getUser(); + kint("hf"); + if (!$account->hasPermission('bypass qashot access')) { + $table = $this->ensureMyTable(); + $grants = db_or(); + foreach (qa_shot_access_grants('view', $account) as $realm => $gids) { + foreach ($gids as $gid) { + $grants->condition(db_and() + ->condition($table . '.gid', $gid) + ->condition($table . '.realm', $realm) + ); + } + } + + $this->query->addWhere('AND', $grants); + $this->query->addWhere('AND', $table . '.grant_view', 1, '>='); + } + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + + $contexts[] = 'user.qa_shot_test_grants:view'; + + return $contexts; + } + +} diff --git a/web/modules/custom/qa_shot/src/Plugin/views/filter/Status.php b/web/modules/custom/qa_shot/src/Plugin/views/filter/Status.php new file mode 100644 index 00000000..c0e3dbea --- /dev/null +++ b/web/modules/custom/qa_shot/src/Plugin/views/filter/Status.php @@ -0,0 +1,53 @@ +ensureMyTable(); + $this->query->addWhereExpression($this->options['group'], "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_QASHOT_TESTS*** = 1) OR ***BYPASS_QASHOT_TEST_ACCESS*** = 1"); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + + $contexts[] = 'user'; + + return $contexts; + } + +} diff --git a/web/modules/custom/qa_shot/src/QAShotGrantDatabaseStorage.php b/web/modules/custom/qa_shot/src/QAShotGrantDatabaseStorage.php new file mode 100644 index 00000000..15c1853b --- /dev/null +++ b/web/modules/custom/qa_shot/src/QAShotGrantDatabaseStorage.php @@ -0,0 +1,299 @@ +database = $database; + $this->moduleHandler = $module_handler; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function access(QAShotTestInterface $qa_shot_test, $operation, AccountInterface $account) { + // Grants only support these operations. + if (!in_array($operation, ['view', 'update', 'delete'])) { + return AccessResult::neutral(); + } + + // Check the database for potential access grants. + $query = $this->database->select('qa_shot_test_access'); + $query->addExpression('1'); + // Only interested for granting in the current operation. + $query->condition('grant_' . $operation, 1, '>='); + // Check for grants for this qa_shot_test and the correct langcode. + $ids = $query->andConditionGroup() + ->condition('id', $qa_shot_test->id()) + ->condition('langcode', $qa_shot_test->language()->getId()); + // If the qa_shot_test is published, also take the default grant into + // account. The default is saved with a qa_shot_test ID of 0. + $status = $qa_shot_test->isPublished(); + if ($status) { + $ids = $query->orConditionGroup() + ->condition($ids) + ->condition('id', 0); + } + $query->condition($ids); + $query->range(0, 1); + + $grants = static::buildGrantsQueryCondition(qa_shot_access_grants($operation, $account)); + + if (count($grants) > 0) { + $query->condition($grants); + } + + // Only the 'view' qa_shot_test grant can currently be cached; the others + // currently don't have any cacheability metadata. Hopefully, we can add + // that in the future, which would allow this access check result to be + // cacheable in all cases. For now, this must remain marked as uncacheable, + // even when it is theoretically cacheable, because we don't have the + // necessary metadata to know it for a fact. + $set_cacheability = function (AccessResult $access_result) use ($operation) { + $access_result->addCacheContexts(['user.qa_shot_test_grants:' . $operation]); + if ($operation !== 'view') { + $access_result->setCacheMaxAge(0); + } + return $access_result; + }; + + if ($query->execute()->fetchField()) { + return $set_cacheability(AccessResult::allowed()); + } + else { + return $set_cacheability(AccessResult::neutral()); + } + } + + /** + * {@inheritdoc} + */ + public function checkAll(AccountInterface $account) { + $query = $this->database->select('qa_shot_test_access'); + $query->addExpression('COUNT(*)'); + $query + ->condition('id', 0) + ->condition('grant_view', 1, '>='); + + $grants = static::buildGrantsQueryCondition(qa_shot_access_grants('view', $account)); + + if (count($grants) > 0) { + $query->condition($grants); + } + return $query->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function alterQuery($query, array $tables, $op, AccountInterface $account, $base_table) { + if (!$langcode = $query->getMetaData('langcode')) { + $langcode = FALSE; + } + + // Find all instances of the base table being joined -- could appear + // more than once in the query, and could be aliased. Join each one to + // the qa_shot_test_access table. + $grants = qa_shot_access_grants($op, $account); + foreach ($tables as $nalias => $tableinfo) { + $table = $tableinfo['table']; + if (!($table instanceof SelectInterface) && $table == $base_table) { + // Set the subquery. + $subquery = $this->database->select('qa_shot_test_access', 'na') + ->fields('na', ['id']); + + // If any grant exists for the specified user, then user has access to + // the qa_shot_test for the specified operation. + $grant_conditions = static::buildGrantsQueryCondition($grants); + + // Attach conditions to the subquery for qa_shot_tests. + if (count($grant_conditions->conditions())) { + $subquery->condition($grant_conditions); + } + $subquery->condition('na.grant_' . $op, 1, '>='); + + // Add langcode-based filtering if this is a multilingual site. + if (\Drupal::languageManager()->isMultilingual()) { + // If no specific langcode to check for is given, use the grant entry + // which is set as a fallback. + // If a specific langcode is given, use the grant entry for it. + if ($langcode === FALSE) { + $subquery->condition('na.fallback', 1, '='); + } + else { + $subquery->condition('na.langcode', $langcode, '='); + } + } + + $field = 'id'; + // Now handle entities. + $subquery->where("$nalias.$field = na.id"); + + $query->exists($subquery); + } + } + } + + /** + * {@inheritdoc} + */ + public function write(QAShotTestInterface $qa_shot_test, array $grants, $realm = NULL, $delete = TRUE) { + if ($delete) { + $query = $this->database->delete('qa_shot_test_access')->condition('id', $qa_shot_test->id()); + if ($realm) { + $query->condition('realm', [$realm, 'all'], 'IN'); + } + $query->execute(); + } + // Only perform work when qa_shot_test_access modules are active. + if (!empty($grants) && count($this->moduleHandler->getImplementations('qa_shot_test_grants'))) { + $query = $this->database->insert('qa_shot_test_access')->fields([ + 'id', 'langcode', 'fallback', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete', + ]); + // If we have defined a granted langcode, use it. But if not, add a grant + // for every language this qa_shot_test is translated to. + foreach ($grants as $grant) { + if ($realm && $realm != $grant['realm']) { + continue; + } + if (isset($grant['langcode'])) { + $grant_languages = [$grant['langcode'] => $this->languageManager->getLanguage($grant['langcode'])]; + } + else { + $grant_languages = $qa_shot_test->getTranslationLanguages(TRUE); + } + foreach ($grant_languages as $grant_langcode => $grant_language) { + // Only write grants; denies are implicit. + if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) { + $grant['id'] = $qa_shot_test->id(); + $grant['langcode'] = $grant_langcode; + // The record with the original langcode is used as the fallback. + if ($grant['langcode'] == $qa_shot_test->language()->getId()) { + $grant['fallback'] = 1; + } + else { + $grant['fallback'] = 0; + } + $query->values($grant); + } + } + } + $query->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function delete() { + $this->database->truncate('qa_shot_test_access')->execute(); + } + + /** + * {@inheritdoc} + */ + public function writeDefault() { + $this->database->insert('qa_shot_test_access') + ->fields([ + 'id' => 0, + 'realm' => 'all', + 'gid' => 0, + 'grant_view' => 0, + 'grant_update' => 0, + 'grant_delete' => 0, + ]) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function count() { + return $this->database->query('SELECT COUNT(*) FROM {qa_shot_test_access}')->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function deleteQAShotRecords(array $ids) { + $this->database->delete('qa_shot_test_access') + ->condition('id', $ids, 'IN') + ->execute(); + } + + /** + * Creates a query condition from an array of qa_shot_test access grants. + * + * @param array $qa_shot_test_access_grants + * An array of grants, as returned by qa_shot_test_access_grants(). + * + * @return \Drupal\Core\Database\Query\Condition + * A condition object to be passed to $query->condition(). + * + * @see qa_shot_test_access_grants() + */ + protected static function buildGrantsQueryCondition(array $qa_shot_test_access_grants) { + $grants = new Condition("OR"); + foreach ($qa_shot_test_access_grants as $realm => $gids) { + if (!empty($gids)) { + $and = new Condition('AND'); + $grants->condition($and + ->condition('gid', $gids, 'IN') + ->condition('realm', $realm) + ); + } + } + + return $grants; + } + +} diff --git a/web/modules/custom/qa_shot/src/QAShotGrantDatabaseStorageInterface.php b/web/modules/custom/qa_shot/src/QAShotGrantDatabaseStorageInterface.php new file mode 100644 index 00000000..68095aa5 --- /dev/null +++ b/web/modules/custom/qa_shot/src/QAShotGrantDatabaseStorageInterface.php @@ -0,0 +1,130 @@ + 'Identifies which realm/grant pairs a user must possess in order to view, update, or delete specific QAShot tests.', + 'fields' => [ + 'id' => [ + 'description' => 'The {qa_shot_test}.id this record affects.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'langcode' => [ + 'description' => 'The {language}.langcode of this QAShot test.', + 'type' => 'varchar_ascii', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ], + 'fallback' => [ + 'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + 'size' => 'tiny', + ], + 'gid' => [ + 'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the QAShot test.", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'realm' => [ + 'description' => 'The realm in which the user must possess the grant ID. Each QAShot test access QAShot test can define one or more realms.', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'grant_view' => [ + 'description' => 'Boolean indicating whether a user with the realm/grant pair can view this QAShot test.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ], + 'grant_update' => [ + 'description' => 'Boolean indicating whether a user with the realm/grant pair can edit this QAShot test.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ], + 'grant_delete' => [ + 'description' => 'Boolean indicating whether a user with the realm/grant pair can delete this QAShot test.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ], + ], + 'primary key' => ['id', 'gid', 'realm', 'langcode'], + 'foreign keys' => [ + 'affected_qa_shot_test' => [ + 'table' => 'qa_shot_test', + 'columns' => ['id' => 'id'], + ], + ], + ]; $schemas = array_merge($schemas, $this->queueSchema()); return $schemas; } diff --git a/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandler.php b/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandler.php index 76325e9c..c12d0a86 100644 --- a/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandler.php +++ b/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandler.php @@ -3,45 +3,230 @@ namespace Drupal\qa_shot; use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; +use Drupal\qa_shot\Entity\QAShotTestInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Access controller for the QAShot Test entity. * * @see \Drupal\qa_shot\Entity\QAShotTest. */ -class QAShotTestAccessControlHandler extends EntityAccessControlHandler { +class QAShotTestAccessControlHandler extends EntityAccessControlHandler implements QAShotTestAccessControlHandlerInterface, EntityHandlerInterface { + + /** + * The qa_shot_test grant storage. + * + * @var \Drupal\qa_shot\QAShotGrantDatabaseStorageInterface + */ + protected $grantStorage; + + /** + * Constructs a NodeAccessControlHandler object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\qa_shot\QAShotGrantDatabaseStorageInterface $grant_storage + * The qa_shot_test grant storage. + */ + public function __construct(EntityTypeInterface $entity_type, QAShotGrantDatabaseStorageInterface $grant_storage) { + parent::__construct($entity_type); + $this->grantStorage = $grant_storage; + } /** * {@inheritdoc} */ - protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - /** @var \Drupal\qa_shot\Entity\QAShotTestInterface $entity */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('qa_shot.grant_storage') + ); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return AccessResult::allowedIfHasPermission($account, 'add qashot test entities'); + } + + /** + * {@inheritdoc} + */ + public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $account = $this->prepareUser($account); + + if ($account->hasPermission('bypass qashot access')) { + $result = AccessResult::allowed()->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + if (!$account->hasPermission('access content')) { + $result = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + $result = parent::access($entity, $operation, $account, TRUE)->cachePerPermissions(); + + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) { + $account = $this->prepareUser($account); + + if ($account->hasPermission('bypass qashot access')) { + $result = AccessResult::allowed()->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + if (!$account->hasPermission('access content')) { + $result = AccessResult::forbidden()->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + if ($account->hasPermission('add qashot test entities')) { + $result = AccessResult::allowed()->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + + return $return_as_object ? AccessResult::neutral() : AccessResult::neutral()->isNeutral(); + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $qa_shot_test, $operation, AccountInterface $account) { + /** @var \Drupal\qa_shot\Entity\QAShotTestInterface $qa_shot_test */ + switch ($operation) { case 'view': - if (!$entity->isPublished()) { - return AccessResult::allowedIfHasPermission($account, 'view unpublished qashot test entities'); + if (!$qa_shot_test->isPublished() && ( + $account->id() == $qa_shot_test->getOwnerId() && $account->hasPermission('view own unpublished qashot test entities') || + $account->hasPermission('view any unpublished qashot test entities') + )) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($qa_shot_test); + } + + if ( + $account->id() == $qa_shot_test->getOwnerId() && $account->hasPermission('view own published qashot test entities') || + $account->hasPermission('view any published qashot test entities') + ) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($qa_shot_test); } - return AccessResult::allowedIfHasPermission($account, 'view published qashot test entities'); + break; case 'update': - return AccessResult::allowedIfHasPermission($account, 'edit qashot test entities'); + if ( + $account->id() == $qa_shot_test->getOwnerId() && $account->hasPermission('edit own qashot test entities') || + $account->hasPermission('edit any qashot test entities') + ) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($qa_shot_test); + } + break; case 'delete': - return AccessResult::allowedIfHasPermission($account, 'delete qashot test entities'); + if ( + $account->id() == $qa_shot_test->getOwnerId() && $account->hasPermission('delete own qashot test entities') || + $account->hasPermission('delete any qashot test entities') + ) { + return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($qa_shot_test); + } + break; } - // Unknown operation, no opinion. - return AccessResult::neutral(); + // Evaluate qa_shot_test grants. + return $this->grantStorage->access($qa_shot_test, $operation, $account); } /** * {@inheritdoc} */ - protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowedIfHasPermission($account, 'add qashot test entities'); + protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { + // Only users with the administer qa_shot_tests permission can edit + // administrative fields. + $administrative_fields = ['uid', 'status', 'created', 'promote', 'sticky']; + if ($operation == 'edit' && in_array($field_definition->getName(), $administrative_fields, TRUE)) { + return AccessResult::allowedIfHasPermission($account, 'administer qashot test entities'); + } + + // No user can change read only fields. + $read_only_fields = ['revision_timestamp', 'revision_uid']; + if ($operation == 'edit' && in_array($field_definition->getName(), $read_only_fields, TRUE)) { + return AccessResult::forbidden(); + } + + // Users have access to the revision_log field either if they have + // administrative permissions or if the new revision option is enabled. + if ($operation == 'edit' && $field_definition->getName() == 'revision_log') { + if ($account->hasPermission('administer qashot test entities')) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::allowedIf($items->getEntity()->type->entity->isNewRevision())->cachePerPermissions(); + } + return parent::checkFieldAccess($operation, $field_definition, $account, $items); + } + + /** + * {@inheritdoc} + */ + public function acquireGrants(QAShotTestInterface $qa_shot_test) { + $grants = $this->moduleHandler->invokeAll('qa_shot_test_access_records', [$qa_shot_test]); + // Let modules alter the grants. + $this->moduleHandler->alter('qa_shot_test_access_records', $grants, $qa_shot_test); + // If no grants are set and the qa_shot_test is published, then use the + // default grant. + if (empty($grants) && $qa_shot_test->isPublished()) { + $grants[] = [ + 'realm' => 'all', + 'gid' => 0, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + ]; + } + return $grants; + } + + /** + * {@inheritdoc} + */ + public function writeGrants(QAShotTestInterface $qa_shot_test, $delete = TRUE) { + $grants = $this->acquireGrants($qa_shot_test); + $this->grantStorage->write($qa_shot_test, $grants, NULL, $delete); + } + + /** + * {@inheritdoc} + */ + public function writeDefaultGrant() { + $this->grantStorage->writeDefault(); + } + + /** + * {@inheritdoc} + */ + public function deleteGrants() { + $this->grantStorage->delete(); + } + + /** + * {@inheritdoc} + */ + public function countGrants() { + return $this->grantStorage->count(); + } + + /** + * {@inheritdoc} + */ + public function checkAllGrants(AccountInterface $account) { + return $this->grantStorage->checkAll($account); } } diff --git a/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandlerInterface.php b/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandlerInterface.php new file mode 100644 index 00000000..4483c8e7 --- /dev/null +++ b/web/modules/custom/qa_shot/src/QAShotTestAccessControlHandlerInterface.php @@ -0,0 +1,84 @@ +t('QAShot Test ID'); $header['name'] = $this->t('Name'); + $header['tags'] = $this->t('Tags'); + $header['test-type'] = $this->t('Test Type'); + $header['status'] = $this->t('Status'); return $header + parent::buildHeader(); } @@ -30,16 +33,23 @@ public function buildHeader() { */ public function buildRow(EntityInterface $entity) { /* @var $entity \Drupal\qa_shot\Entity\QAShotTest */ - $row['id'] = $entity->id(); - $row['name'] = $this->l( - $entity->label(), - new Url( - 'entity.qa_shot_test.edit_form', [ - 'qa_shot_test' => $entity->id(), - ] - ) - ); - return $row + parent::buildRow($entity); + if ($entity->access('view')) { + $row['id'] = $entity->id(); + $row['name'] = $this->l( + $entity->label(), + new Url( + 'entity.qa_shot_test.edit_form', [ + 'qa_shot_test' => $entity->id(), + ] + ) + ); + $row['tags'] = implode(', ', $entity->getCacheTags()); + $row['test-type'] = $entity->getType(); + $row['status'] = $entity->getHumanReadableQueueStatus(); + return $row + parent::buildRow($entity); + } + + return NULL; } } diff --git a/web/modules/custom/qa_shot/src/QAShotTestRunAccessControl.php b/web/modules/custom/qa_shot/src/QAShotTestRunAccessControl.php new file mode 100644 index 00000000..2599e2db --- /dev/null +++ b/web/modules/custom/qa_shot/src/QAShotTestRunAccessControl.php @@ -0,0 +1,43 @@ +id(); + // If user 1. + if ($account_id == 1) { + return AccessResult::allowed(); + } + + /* @var $node \Drupal\qa_shot\Entity\QAShotTestInterface */ + $node = \Drupal::routeMatch()->getParameter('qa_shot_test'); + + // Own content. + if ($node->getOwnerId() == $account_id) { + return AccessResult::allowedIfHasPermission($account, 'run own qashot test entities'); + } + + // Run any. + return AccessResult::allowedIfHasPermission($account, 'run any qashot test entities'); + } + +} diff --git a/web/modules/custom/qa_shot_rest_api/qa_shot_rest_api.permissions.yml b/web/modules/custom/qa_shot_rest_api/qa_shot_rest_api.permissions.yml index 4a5717ab..72b44e4f 100644 --- a/web/modules/custom/qa_shot_rest_api/qa_shot_rest_api.permissions.yml +++ b/web/modules/custom/qa_shot_rest_api/qa_shot_rest_api.permissions.yml @@ -6,17 +6,35 @@ administer qashot test entities via rest: description: 'Allow to access the administration form to configure QAShot Test entities via rest.' restrict access: true -delete qashot test entities via rest: - title: 'Delete QAShot Test entities via rest' +delete own qashot test entities via rest: + title: 'Delete own QAShot Test entities via rest' -edit qashot test entities via rest: - title: 'Edit QAShot Test entities via rest' +delete any qashot test entities via rest: + title: 'Delete any QAShot Test entities via rest' + +edit own qashot test entities via rest: + title: 'Edit own QAShot Test entities via rest' + +edit any qashot test entities via rest: + title: 'Edit any QAShot Test entities via rest' + +run own qashot test entities via rest: + title: 'Run own QAShot Test entities via rest' + +run any qashot test entities via rest: + title: 'Run any QAShot Test entities via rest' access qashot test overview via rest: title: 'Access the QAShot Test overview page via rest' -view published qashot test entities via rest: - title: 'View published QAShot Test entities via rest' +view own published qashot test entities via rest: + title: 'View own published QAShot Test entities via rest' + +view any published qashot test entities via rest: + title: 'View any published QAShot Test entities via rest' + +view own unpublished qashot test entities via rest: + title: 'View own unpublished QAShot Test entities via rest' -view unpublished qashot test entities via rest: - title: 'View unpublished QAShot Test entities via rest' +view any unpublished qashot test entities via rest: + title: 'View any unpublished QAShot Test entities via rest' diff --git a/web/modules/custom/qa_shot_rest_api/src/Controller/ApiController.php b/web/modules/custom/qa_shot_rest_api/src/Controller/ApiController.php index 662479b5..ba956110 100644 --- a/web/modules/custom/qa_shot_rest_api/src/Controller/ApiController.php +++ b/web/modules/custom/qa_shot_rest_api/src/Controller/ApiController.php @@ -120,6 +120,12 @@ public function queueTest(Request $request): JsonResponse { /** @var \Drupal\qa_shot\Entity\QAShotTestInterface $entity */ $entity = $this->loadEntityFromId($request->attributes->get('qa_shot_test')); + $cur_user = \Drupal::currentUser(); + if (!($entity->getOwnerId() == $cur_user->id() && $cur_user->hasPermission('run own qashot test entities') || + $cur_user->hasPermission('run any qashot test entities'))) { + throw new BadRequestHttpException('You don\'t have access one or more entities from the list. Please remove it/them and try again.'); + } + $storedFrontendUrl = $entity->getFrontendUrl(); if (NULL === $storedFrontendUrl || $storedFrontendUrl !== $frontendUrl) { $entity->setFrontendUrl($frontendUrl); @@ -161,8 +167,12 @@ public function queueTest(Request $request): JsonResponse { * The response. */ public function loginTest(Request $request): JsonResponse { + $user = \Drupal::currentUser(); $responseData = [ 'status' => 'success', + 'uid' => $user->id(), + 'roles' => $user->getRoles(), + 'name' => $user->getAccountName(), ]; return new JsonResponse($responseData); @@ -311,6 +321,7 @@ public function testList(Request $request): JsonResponse { $limit = $limit < 0 ? 0 : $limit; $type = $request->query->get('type', ''); $condition = []; + $account = \Drupal::currentUser(); $queryStartIndex = ($page - 1) * $limit; @@ -319,6 +330,10 @@ public function testList(Request $request): JsonResponse { $query->condition('type', $type, '='); $condition['type'] = ['value' => $type, 'operator' => '=']; } + if (in_array('qas_sub', $account->getRoles(TRUE), FALSE)) { + $query->condition('user_id', $account->id(), '='); + $condition['user_id'] = ['value' => $account->id(), 'operator' => '=']; + } $testsIds = $query->execute(); $tests = $this->testStorage->loadMultiple($testsIds); @@ -368,13 +383,20 @@ public function getQueueStatus(Request $request): JsonResponse { throw new BadRequestHttpException('The \'tids\' parameter is empty or not an array.'); } - /** @var array $ids */ + /** @var array $tids */ $tids = $runnerSettings['tids']; + $entities = $this->testStorage->loadMultiple($tids); $queueData = []; $notInQueue = []; + $cur_user = \Drupal::currentUser(); foreach ($tids as $tid) { + if (!$entities[$tid]->access('view', $cur_user)) { + throw new BadRequestHttpException('You don\'t have access one or more entities from the list. Please remove it/them and try again.'); + } + $data = $this->queueData->getDataFromQueue($tid); + if ($data) { $queueData[] = $data; } @@ -437,10 +459,14 @@ public function getLastModification(Request $request): JsonResponse { $responseEntities = []; $noChanges = []; + $cur_user = \Drupal::currentUser(); foreach ($requestedEntities as $requestedEntity) { if ($entities[$requestedEntity['tid']]) { - $testAsArray = $this->serializer->normalize($entities[$requestedEntity['tid']]); + if (!$entities[$requestedEntity['tid']]->access('view', $cur_user)) { + throw new BadRequestHttpException('You don\'t have access one or more entities from the list. Please remove it/them and try again.'); + } + $testAsArray = $this->serializer->normalize($entities[$requestedEntity['tid']]); if ($testAsArray['changed'] > $requestedEntity['changed']) { $responseEntities[] = $testAsArray; } @@ -550,7 +576,7 @@ public function access(AccountInterface $account): AccessResult { return AccessResult::allowed(); } - return AccessResult::allowedIf(in_array('rest_api_user', $account->getRoles(TRUE), FALSE)); + return AccessResult::allowedIf(in_array('rest_api_user', $account->getRoles(TRUE), FALSE) || in_array('qas_sub', $account->getRoles(TRUE), FALSE)); } }