Skip to content

Commit

Permalink
Extract user deletion into a new service (#919)
Browse files Browse the repository at this point in the history
* Extract user deletion into a new service

Introduce a programming API for downstream developers that allows
customizing what should happen when a user is deleted in Drupal.

Co-authored-by: Dezső BICZÓ <[email protected]>

* Leverage the new user removal handler API for cascade deleting team roles

* Better naming for the new API and implementations

---------

Co-authored-by: ferenc.csongradi <[email protected]>
  • Loading branch information
mxr576 and FCsongradi authored Sep 1, 2023
1 parent 230b0c5 commit a7b3ef7
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 47 deletions.
26 changes: 2 additions & 24 deletions apigee_edge.module
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ function apigee_edge_entity_view(array &$build, EntityInterface $entity, EntityV
],
],
]))->toRenderable();

$build['#attached']['library'][] = 'core/drupal.dialog.ajax';
}
}
Expand Down Expand Up @@ -1490,29 +1490,7 @@ function apigee_edge_user_cancel_methods_alter(&$methods) {
* Implements hook_user_delete().
*/
function apigee_edge_user_delete(UserInterface $account) {
// Do not try to delete developer of the anonymous user because it does
// not exist.
if ($account->isAnonymous()) {
return;
}

try {
/** @var \Drupal\apigee_edge\Entity\Developer $developer */
$developer = Developer::load($account->getEmail());
// Sanity check, the developer may not exist in Apigee Edge.
if ($developer) {
$developer->delete();
}
}
catch (\Exception $exception) {
$context = [
'@developer' => $account->getEmail(),
'@message' => (string) $exception,
];
// @todo watchdog_exception() function has been deprecated for Drupal 10.1 https://www.drupal.org/node/2932520
// @phpstan-ignore-next-line
watchdog_exception('apigee_edge', $exception, 'Could not delete @developer developer entity. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
}
(\Drupal::service('apigee_edge.post_user_delete_action_performer'))($account);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions apigee_edge.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,7 @@ services:
apigee_edge.authentication.oauth_token_storage:
class: Drupal\apigee_edge\OauthTokenFileStorage
arguments: ['@config.factory', '@file_system', '@logger.channel.apigee_edge']

apigee_edge.post_user_delete_action_performer:
class: Drupal\apigee_edge\User\RemoveRelatedDeveloperAccountSynchronousPostUserDeleteActionPerformer
arguments: ['@entity_type.manager', '@logger.channel.apigee_edge']
23 changes: 0 additions & 23 deletions modules/apigee_edge_teams/apigee_edge_teams.module
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,6 @@ function _apigee_edge_teams_team_app_entity_form_display_edit_form_validate(arra
}
}

/**
* Implements hook_ENTITY_TYPE_delete().
*/
function apigee_edge_teams_user_delete(EntityInterface $entity) {
/** @var \Drupal\user\UserInterface $entity */
/** @var \Drupal\apigee_edge_teams\Entity\Storage\TeamMemberRoleStorageInterface $team_member_role_storage */
$team_member_role_storage = \Drupal::entityTypeManager()->getStorage('team_member_role');
// When a user gets deleted then its developer account also gets deleted
// from Apigee Edge which removes its (team) company memberships.
// We must delete this user's team roles from Drupal as well.
foreach ($team_member_role_storage->loadByDeveloper($entity) as $team_member_roles_in_team) {
try {
$team_member_roles_in_team->delete();
}
catch (EntityStorageException $e) {
\Drupal::logger('apigee_edge_teams')->critical("Integrity check: Failed to remove %developer team member's roles in %team team when its Drupal user got deleted.", [
'%developer' => $entity->getEmail(),
'%team' => $team_member_roles_in_team->getTeam()->id(),
]);
}
}
}

/**
* Implements hook_ENTITY_TYPE_delete().
*/
Expand Down
5 changes: 5 additions & 0 deletions modules/apigee_edge_teams/apigee_edge_teams.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,8 @@ services:
apigee_edge_teams.cli:
class: Drupal\apigee_edge_teams\CliService
arguments: ['@apigee_edge.apigee_edge_mgmt_cli_service']

apigee_edge_teams.post_user_delete_action_performer:
class: Drupal\apigee_edge_teams\User\RemoveTeamRolesOfUserSynchronousPostUserDeleteActionPerformer
decorates: apigee_edge.post_user_delete_action_performer
arguments: [ '@apigee_edge_teams.post_user_delete_action_performer.inner', '@entity_type.manager', '@logger.channel.apigee_edge_teams' ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

/**
* Copyright 2023 Google Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

namespace Drupal\apigee_edge_teams\User;

use Drupal\apigee_edge\User\PostUserDeleteActionPerformerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Utility\Error;
use Drupal\user\UserInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;

/**
* Ensures team roles of the removed user also get deleted.
*/
final class RemoveTeamRolesOfUserSynchronousPostUserDeleteActionPerformer implements PostUserDeleteActionPerformerInterface {

/**
* Constructs a new object.
*/
public function __construct(private readonly PostUserDeleteActionPerformerInterface $decorated, private readonly EntityTypeManagerInterface $entityTypeManager, private readonly LoggerInterface $logger) {}

/**
* {@inheritdoc}
*/
public function __invoke(UserInterface $user): void {
($this->decorated)($user);

/** @var \Drupal\apigee_edge_teams\Entity\Storage\TeamMemberRoleStorageInterface $team_member_role_storage */
$team_member_role_storage = $this->entityTypeManager->getStorage('team_member_role');
// When a user gets deleted then its developer account also gets deleted
// from Apigee Edge which removes its (team) company memberships.
// We must delete this user's team roles from Drupal as well.
foreach ($team_member_role_storage->loadByDeveloper($user) as $team_member_roles_in_team) {
try {
$team_member_roles_in_team->delete();
}
catch (\Exception $e) {
Error::logException($this->logger, $e, "Integrity check: Failed to remove %developer team member's roles in %team team when its Drupal user got deleted. Reason: @message", [
'%developer' => $user->getEmail(),
'%team' => $team_member_roles_in_team->getTeam()->id(),
], LogLevel::CRITICAL);
}
}
}

}
44 changes: 44 additions & 0 deletions src/User/PostUserDeleteActionPerformerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* Copyright 2023 Google Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

namespace Drupal\apigee_edge\User;

use Drupal\user\UserInterface;

/**
* Contract for triggering Apigee specific reactions _after_ user removal.
*
* It is important to know that when implementations are called the user
* entity is already removed and the database transaction is closed, there
* is no way to roll that back.
*/
interface PostUserDeleteActionPerformerInterface {

/**
* React on user removal.
*
* @param \Drupal\user\UserInterface $user
* The deleted user.
*
* @see \hook_ENTITY_TYPE_delete()
*/
public function __invoke(UserInterface $user): void;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

/**
* Copyright 2023 Google Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

namespace Drupal\apigee_edge\User;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Utility\Error;
use Drupal\user\UserInterface;
use Psr\Log\LoggerInterface;

/**
* Handler that removes the related developer when a user is deleted.
*
* ATTENTION!!! Removing a developer from Apigee is a dangerous operation
* because it also destroys all API keys the developer pwns. If that is not
* an intended behavior, use the service decorator pattern to customize
* this process.
*
* @see \apigee_edge_user_delete()
*/
final class RemoveRelatedDeveloperAccountSynchronousPostUserDeleteActionPerformer implements PostUserDeleteActionPerformerInterface {

/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private EntityTypeManagerInterface $entityTypeManager;

/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
private LoggerInterface $logger;

/**
* Constructs a new object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger) {
$this->entityTypeManager = $entity_type_manager;
$this->logger = $logger;
}

/**
* {@inheritdoc}
*/
public function __invoke(UserInterface $user): void {
// Do not try to delete developer of the anonymous user because it does
// not exist.
if ($user->isAnonymous()) {
return;
}

try {
/** @var \Drupal\apigee_edge\Entity\DeveloperInterface|null $developer */
$developer = $this->entityTypeManager->getStorage('developer')->load($user->getEmail());
// Sanity check, the developer may not exist in Apigee Edge.
if ($developer) {
$developer->delete();
$this->logger->info('The @developer developer has been deleted as a reaction to removing its user account.', [
'@developer' => $user->getEmail(),
]);
}
}
catch (\Exception $exception) {
$context = [
'@developer' => $user->getEmail(),
];
Error::logException($this->logger, $exception, 'The @developer developer could not be deleted as a reaction to removing its user account. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
}
}

}

0 comments on commit a7b3ef7

Please sign in to comment.