Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Cleanup class #47

Merged
merged 7 commits into from
Feb 13, 2025
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 173 additions & 100 deletions src/Cleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

class Cleanup
{
private const OWNERSHIP = 'OWNERSHIP';
private const USER = 'USER';
private const ROLE = 'ROLE';

public function __construct(
readonly Config $config,
readonly Connection $sourceConnection,
Expand All @@ -33,7 +37,14 @@ public function sourceAccount(): void
$databaseRole = $this->sourceConnection->getOwnershipRoleOnDatabase($database);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Owner DB je "project role".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KEBOOLA_123

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jo je...jo je to role která vlastní databázi, ano projektová role no

$projectUser = $this->getProjectUser($databaseRole);

$data = $this->getDataToRemove($this->sourceConnection, $databaseRole);
$grantedOnDatabaseRole = $this->sourceConnection->fetchAll(sprintf(
'SHOW GRANTS ON ROLE %s',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Myslel jsem, že jet o TO ROLE? ale funguje podle dokumentace oboje :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tohle teda vezme všechny granty, která má project role

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOW GRANTS ON ROLE KEBOOLA_123

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tohle souvisí i s tím druhým commentem
SHOW GRANT TO ROLE KEBOOLA_123 - načte všechny granty vlastněné touto rolí
image

SHOW GRANT ON ROLE KEBOOLA_123 - načte granty které vlastní tuto roli - takže KEBOOLA_STORAGE
image

Helper::quoteIdentifier($databaseRole)
));
assert(count($grantedOnDatabaseRole) === 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nrozumím proč je jen jeden ten grant. Může jich být imho klidně víc - třeba na ws usery, schemata, ws role, etc., ne?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jako je super, že je tu assert, ale nerozumím proč to funguje

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funguje to protože - #47 (comment)

$mainRoleName = current($grantedOnDatabaseRole)['granted_by'];

$data = $this->getDataToRemove($this->sourceConnection, $databaseRole, $mainRoleName);

// drop roles
$sqls[] = sprintf('DROP ROLE %s;', Helper::quoteIdentifier($databaseRole));
Expand Down Expand Up @@ -87,13 +98,12 @@ public function sourceAccount(): void

public function preMigration(string $mainRoleName): void
{
$this->logger->info(sprintf('Starting pre-migration cleanup with main role: %s', $mainRoleName));
$sqls = [];
$currentRole = $this->config->getTargetSnowflakeRole();
$currentRole = null;

$mainRole = $this->destinationConnection->fetchAll(sprintf(
'SHOW ROLES LIKE %s',
QueryBuilder::quote($mainRoleName),
));
// Check if main role is assigned to target user
$this->logger->info('Checking main role assignment and ownership');
$grantsToTargetUser = $this->destinationConnection->fetchAll(sprintf(
'SHOW GRANTS TO USER %s',
QueryBuilder::quoteIdentifier($this->config->getTargetSnowflakeUser()),
Expand All @@ -104,6 +114,11 @@ public function preMigration(string $mainRoleName): void
false,
);

// Check if target role has ownership of main role
$mainRole = $this->destinationConnection->fetchAll(sprintf(
'SHOW ROLES LIKE %s',
QueryBuilder::quote($mainRoleName),
));
$hasMainRoleOwnership = array_reduce(
$mainRole,
fn ($found, $v) => $found || $v['owner'] === $this->config->getTargetSnowflakeRole(),
Expand All @@ -118,123 +133,117 @@ public function preMigration(string $mainRoleName): void
}
$this->destinationConnection->grantRoleToUser($this->config->getTargetSnowflakeUser(), $mainRoleName);
}

// Process each database
foreach ($this->config->getDatabases() as $database) {
$this->logger->info(sprintf('Processing database: %s', $database));
$this->destinationConnection->useRole($this->config->getTargetSnowflakeRole());

// Check if database exists
$dbExists = $this->destinationConnection->fetchAll(sprintf(
'SHOW DATABASES LIKE %s;',
QueryBuilder::quote($database)
));

if (!$dbExists) {
continue;
}
$databaseRole = $this->destinationConnection->getOwnershipRoleOnDatabase($database);
$data = $this->getDataToRemove($this->destinationConnection, $databaseRole);

$currentUser = $this->destinationConnection->fetchAll('SELECT CURRENT_USER() AS "user";');
foreach ($data['USER'] ?? [] as $user) {
if ($user['granted_by'] !== $currentRole) {
$currentRole = $user['granted_by'];
if ($currentRole === $mainRoleName && !$mainRoleExistsOnTargetUser) {
$sqls[] = sprintf(
'GRANT ROLE %s TO USER %s;',
Helper::quoteIdentifier($currentRole),
Helper::quoteIdentifier($this->config->getTargetSnowflakeUser())
);
$this->logger->info(sprintf('Database %s does not exist, checking for role with same name', $database));
// Check if role exists with exact or lowercase name
$roleName = null;
foreach ([$database, strtolower($database)] as $nameVariant) {
$roleExists = $this->destinationConnection->fetchAll(sprintf(
'SHOW ROLES LIKE %s',
QueryBuilder::quote($nameVariant)
));
if ($roleExists) {
$roleName = $nameVariant;
break;
}
$sqls[] = sprintf('USE ROLE %s;', Helper::quoteIdentifier($currentRole));
}

if ($currentUser[0]['user'] !== $user['name']) {
$sqls[] = sprintf('DROP USER IF EXISTS %s;', Helper::quoteIdentifier($user['name']));
if ($roleName === null) {
continue;
}
}

foreach ($data['ROLE'] ?? [] as $role) {
if ($role['granted_by'] !== $currentRole) {
$currentRole = $role['granted_by'];
$sqls[] = sprintf('USE ROLE %s;', Helper::quoteIdentifier($currentRole));
}
$this->destinationConnection->useRole($mainRoleName);
if ($currentRole === $mainRoleName && !$mainRoleExistsOnTargetUser) {
$this->destinationConnection->grantRoleToUser(
$this->config->getTargetSnowflakeUser(),
$currentRole,
);
}
try {
$this->destinationConnection->useRole($role['granted_by']);
} catch (RuntimeException $e) {
$this->destinationConnection->grantRoleToUser(
$this->config->getTargetSnowflakeUser(),
$role['granted_by'],
);
$this->destinationConnection->useRole($role['granted_by']);
}
$dataToRemove = $this->getDataToRemove($this->destinationConnection, $roleName, $mainRoleName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A co projektový user KEBOOLA_123?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jestli myslíš smazání ho tak se přidává v metodě "addProjectRoleAndUserToRemove"

} else {
$this->logger->info(sprintf('Database %s exists, getting ownership role', $database));
$databaseRole = $this->destinationConnection->getOwnershipRoleOnDatabase($database);
$dataToRemove = $this->getDataToRemove($this->destinationConnection, $databaseRole, $mainRoleName);
}

/** @var FutureGrantToRole[] $futureGrants */
// First revoke all future grants from roles
foreach ($dataToRemove['ROLE'] as $role) {
$this->destinationConnection->useRole($role['granted_by']);
$futureGrants = array_map(
fn(array $v) => FutureGrantToRole::fromArray($v),
$this->destinationConnection->fetchAll(sprintf(
'SHOW FUTURE GRANTS TO ROLE %s',
$role['name']
Helper::quoteIdentifier($role['name'])
))
);

if ($futureGrants) {
[$sqls, $currentRole] = $this->switchRole($role['granted_by'], $mainRoleName, $sqls, $currentRole);
}
foreach ($futureGrants as $futureGrant) {
$sqls[] = sprintf(
'REVOKE %s ON FUTURE TABLES IN SCHEMA %s FROM ROLE %s;',
$futureGrant->getPrivilege(),
$futureGrant->getName(),
Helper::quoteIdentifier($futureGrant->getGranteeName()),
Helper::quoteIdentifier($futureGrant->getGranteeName())
);
}
$sqls[] = sprintf('DROP ROLE IF EXISTS %s;', Helper::quoteIdentifier($role['name']));
}

$projectUser = $this->getProjectUser($databaseRole);

if ($projectUser->getGrantedBy() !== $currentRole) {
$currentRole = $projectUser->getGrantedBy();
if ($currentRole === $mainRoleName && !$mainRoleExistsOnTargetUser) {
$sqls[] = sprintf(
'USE ROLE %s;',
Helper::quoteIdentifier($this->config->getTargetSnowflakeRole())
);
$sqls[] = sprintf(
'GRANT ROLE %s TO USER %s;',
Helper::quoteIdentifier($currentRole),
Helper::quoteIdentifier($this->config->getTargetSnowflakeUser())
);
}
$sqls[] = sprintf('USE ROLE %s;', Helper::quoteIdentifier($currentRole));
// Drop users owned by roles
if (!empty($dataToRemove['USER'])) {
ondrajodas marked this conversation as resolved.
Show resolved Hide resolved
$this->logger->info(sprintf('Dropping %d users', count($dataToRemove['USER'])));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tady bych zalogoval které přesně to dropuje, ne?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to samé - #47 (comment)

}
foreach ($dataToRemove['USER'] as $user) {
[$sqls, $currentRole] = $this->switchRole($user['granted_by'], $mainRoleName, $sqls, $currentRole);
$sqls[] = sprintf(
'DROP USER IF EXISTS %s;',
Helper::quoteIdentifier($user['name'])
);
}
$sqls[] = sprintf(
'DROP USER IF EXISTS %s;',
Helper::quoteIdentifier($projectUser->getGranteeName())
);

$sqls[] = sprintf('DROP ROLE IF EXISTS %s;', Helper::quoteIdentifier($databaseRole));
// Drop roles
if (!empty($dataToRemove['ROLE'])) {
$this->logger->info(sprintf('Dropping %d roles', count($dataToRemove['ROLE'])));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taky bych si myslel, že by tu mělo být které přesně

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no nevím jestli bych to tu měl vypsat 🤷‍♂️ seznam který chceme migrovat těch rolí a userů má několik stovek/tisíc

}
foreach ($dataToRemove['ROLE'] as $role) {
[$sqls, $currentRole] = $this->switchRole($role['granted_by'], $mainRoleName, $sqls, $currentRole);
$sqls[] = sprintf(
'DROP ROLE IF EXISTS %s;',
Helper::quoteIdentifier($role['name'])
);
}

$sqls[] = sprintf(
'DROP DATABASE IF EXISTS %s;',
Helper::quoteIdentifier($database . '_OLD')
);
// Drop database role and handle database if exists
[$sqls, $currentRole] = $this->switchRole($mainRoleName, $mainRoleName, $sqls, $currentRole);

$sqls[] = sprintf(
'ALTER DATABASE IF EXISTS %s RENAME TO %s;',
Helper::quoteIdentifier($database),
Helper::quoteIdentifier($database . '_OLD'),
);
if ($dbExists) {
$sqls[] = sprintf(
'DROP DATABASE IF EXISTS %s;',
Helper::quoteIdentifier($database . '_OLD')
);
$sqls[] = sprintf(
'ALTER DATABASE IF EXISTS %s RENAME TO %s;',
Helper::quoteIdentifier($database),
Helper::quoteIdentifier($database . '_OLD')
);
}
}

$this->destinationConnection->useRole($mainRoleName);
// Execute all SQL commands
foreach ($sqls as $sql) {
if ($this->config->getSynchronizeDryPremigrationCleanupRun()) {
$this->logger->info($sql);
} else {
$this->destinationConnection->query($sql);
}
}

if ($this->config->getSynchronizeDryPremigrationCleanupRun() && $sqls) {
throw new UserException('!!! PLEASE RUN SQLS ON TARGET SNOWFLAKE ACCOUNT !!!');
}
Expand Down Expand Up @@ -297,36 +306,100 @@ public function postMigration(): void
}
}

private function getDataToRemove(Connection $connection, string $role): array
private function getDataToRemove(Connection $connection, string $name, string $mainRoleName): array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tyjo, ten naming... $roleName, ne?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a getDataToRemoveForRole

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming zajišťoval cursor, tak jsem se svezl... přejmenuju

{
$this->logger->debug(sprintf('Getting data to remove for: %s', $name));
ondrajodas marked this conversation as resolved.
Show resolved Hide resolved
$result = [
self::ROLE => [],
self::USER => [],
];

$grants = $connection->fetchAll(sprintf(
'SHOW GRANTS TO ROLE %s',
Helper::quoteIdentifier($role)
Helper::quoteIdentifier($name)
));

$filteredGrants = array_filter($grants, function ($v) {
$usageWarehouse = $v['privilege'] === 'USAGE' && $v['granted_on'] === 'WAREHOUSE';
$ownership = $v['privilege'] === 'OWNERSHIP' && (in_array($v['granted_on'], ['USER', 'ROLE']));
$ownershipGrants = array_filter(
$grants,
fn($v) => $v['privilege'] === self::OWNERSHIP
);

return $ownership || $usageWarehouse;
});
$result = $this->processOwnershipGrants($ownershipGrants, $result);
$result = $this->addUserIfExists($connection, $name, $mainRoleName, $result);
$result = $this->addRoleToRemove($name, $mainRoleName, $result);

$mapGrants = [];
foreach ($filteredGrants as $filteredGrant) {
$mapGrants[$filteredGrant['granted_on']][$filteredGrant['name']] = $filteredGrant;
}
return $result;
}

if (isset($mapGrants['ROLE'])) {
$roleGrants = $mapGrants['ROLE'];
foreach ($roleGrants as $roleGrant) {
$mapGrants = array_merge_recursive(
$this->getDataToRemove($connection, $roleGrant['name']),
$mapGrants,
);
private function processOwnershipGrants(array $ownershipGrants, array $result): array
{
foreach ($ownershipGrants as $grant) {
if ($grant['granted_on'] === self::ROLE) {
$this->logger->debug(sprintf('Found owned role: %s', $grant['name']));
$result[self::ROLE][] = $grant;
} elseif ($grant['granted_on'] === self::USER) {
$this->logger->debug(sprintf('Found directly owned user: %s', $grant['name']));
$result[self::USER][] = $grant;
}
}
return $result;
}

private function addUserIfExists(Connection $connection, string $name, string $mainRoleName, array $result): array
ondrajodas marked this conversation as resolved.
Show resolved Hide resolved
{
$userExists = $connection->fetchAll(sprintf(
'SHOW USERS LIKE %s',
QueryBuilder::quote($name)
));
if ($userExists) {
$this->logger->debug(sprintf('Found user with same name: %s', $name));
$userExists[0]['granted_by'] = $mainRoleName;
ondrajodas marked this conversation as resolved.
Show resolved Hide resolved
$result[self::USER][] = $userExists[0];
}
return $result;
}

private function addRoleToRemove(string $name, string $mainRoleName, array $result): array
{
$result[self::ROLE][] = [
'name' => $name,
'granted_by' => $mainRoleName,
];
return $result;
}

private function switchRole(string $role, string $mainRoleName, array $sqls, ?string $currentRole): array
{
if ($currentRole === $role) {
return [$sqls, $currentRole];
}

return $mapGrants;
try {
$this->logger->debug(sprintf('Switching to role: %s', $role));
$this->destinationConnection->useRole($role);
$sqls[] = sprintf('USE ROLE %s;', Helper::quoteIdentifier($role));
$currentRole = $role;
} catch (RuntimeException $e) {
$this->logger->info(sprintf(
'Cannot switch directly to role %s, trying through main role %s',
$role,
$mainRoleName
));
$this->destinationConnection->useRole($mainRoleName);
if ($currentRole !== $mainRoleName) {
$sqls[] = sprintf('USE ROLE %s;', Helper::quoteIdentifier($mainRoleName));
$currentRole = $mainRoleName;
}
$this->destinationConnection->grantRoleToUser(
$this->config->getTargetSnowflakeUser(),
$role
);
$this->logger->debug(sprintf('Granted role %s to user, switching to it', $role));
$this->destinationConnection->useRole($role);
$sqls[] = sprintf('USE ROLE %s;', Helper::quoteIdentifier($role));
$currentRole = $role;
}
return [$sqls, $currentRole];
}

private function getProjectUser(string $databaseRole): GrantToUser
Expand Down