-
Notifications
You must be signed in to change notification settings - Fork 0
[DPE-7322] Support predefined roles #34
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
base: dpe
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -19,6 +19,10 @@ | |||||
if typing.TYPE_CHECKING: | ||||||
from ..relations import database_requires | ||||||
|
||||||
_ROLE_DML = "charmed_dml" | ||||||
_ROLE_READ = "charmed_read" | ||||||
_ROLE_MAX_LENGTH = 32 | ||||||
|
||||||
logger = logging.getLogger(__name__) | ||||||
|
||||||
|
||||||
|
@@ -123,17 +127,67 @@ def _get_attributes(self, additional_attributes: dict = None) -> str: | |||||
attributes.update(additional_attributes) | ||||||
return json.dumps(attributes) | ||||||
|
||||||
def create_application_database_and_user(self, *, username: str, database: str) -> str: | ||||||
"""Create database and user for related database_provides application.""" | ||||||
# TODO python3.10 min version: Use `set` instead of `typing.Set` | ||||||
def _get_mysql_roles(self, name_pattern: str) -> typing.Set[str]: | ||||||
"""Returns a set with the MySQL roles.""" | ||||||
logger.debug(f"Getting MySQL roles with {name_pattern=}") | ||||||
output_file = self._container.path("/tmp/mysqlsh_output.json") | ||||||
self._run_code( | ||||||
_jinja_env.get_template("get_mysql_roles_with_pattern.py.jinja").render( | ||||||
name_pattern=name_pattern, | ||||||
output_filepath=output_file.relative_to_container, | ||||||
) | ||||||
) | ||||||
with output_file.open("r") as file: | ||||||
rows = json.load(file) | ||||||
output_file.unlink() | ||||||
logger.debug(f"MySQL roles found for {name_pattern=}: {len(rows)}") | ||||||
return {row[0] for row in rows} | ||||||
|
||||||
def _create_application_database(self, *, database: str, rolename: str) -> str: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
nit; return value unused |
||||||
"""Create database for related database_provides application.""" | ||||||
statements = [ | ||||||
f"CREATE DATABASE IF NOT EXISTS `{database}`", | ||||||
f"CREATE ROLE IF NOT EXISTS `{rolename}`", | ||||||
f"GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON `{database}`.* TO {rolename}", | ||||||
f"GRANT ALTER, ALTER ROUTINE, CREATE, CREATE ROUTINE, CREATE VIEW, DROP, INDEX, LOCK TABLES, REFERENCES, TRIGGER ON `{database}`.* TO {rolename}", | ||||||
] | ||||||
|
||||||
mysql_roles = self._get_mysql_roles("charmed_%") | ||||||
if _ROLE_READ in mysql_roles: | ||||||
statements.append( | ||||||
f"GRANT SELECT ON `{database}`.* TO {_ROLE_READ}", | ||||||
) | ||||||
if _ROLE_DML in mysql_roles: | ||||||
statements.append( | ||||||
f"GRANT SELECT, INSERT, DELETE, UPDATE ON `{database}`.* TO {_ROLE_DML}", | ||||||
) | ||||||
|
||||||
logger.debug(f"Creating {database=}") | ||||||
self._run_sql(statements) | ||||||
logger.debug(f"Created {database=}") | ||||||
return database | ||||||
|
||||||
def _create_application_user(self, *, database: str, username: str) -> str: | ||||||
"""Create database user for related database_provides application.""" | ||||||
attributes = self._get_attributes() | ||||||
logger.debug(f"Creating {database=} and {username=} with {attributes=}") | ||||||
password = utils.generate_password() | ||||||
logger.debug(f"Creating {username=} with {attributes=}") | ||||||
self._run_sql([ | ||||||
f"CREATE DATABASE IF NOT EXISTS `{database}`", | ||||||
f"CREATE USER `{username}` IDENTIFIED BY '{password}' ATTRIBUTE '{attributes}'", | ||||||
f"GRANT ALL PRIVILEGES ON `{database}`.* TO `{username}`", | ||||||
]) | ||||||
logger.debug(f"Created {database=} and {username=} with {attributes=}") | ||||||
logger.debug(f"Created {username=} with {attributes=}") | ||||||
return password | ||||||
|
||||||
def create_application_database(self, *, database: str, username: str) -> str: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
"""Create both the database and the relation user, returning its password.""" | ||||||
rolename = f"charmed_dba_{database}" | ||||||
if len(rolename) >= _ROLE_MAX_LENGTH: | ||||||
raise ValueError("Database DBA role longer than 32 characters") | ||||||
|
||||||
________ = self._create_application_database(database=database, rolename=rolename) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
nit |
||||||
password = self._create_application_user(database=database, username=username) | ||||||
return password | ||||||
|
||||||
def add_attributes_to_mysql_router_user( | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import json | ||
|
||
result = session.run_sql( | ||
"SELECT user FROM mysql.user WHERE user LIKE '{{ name_pattern }}'" | ||
) | ||
rows = result.fetch_all() | ||
# mysqlsh objects are weird—they quack (i.e. duck typing) like standard Python objects (e.g. list, | ||
# dict), but do not serialize to JSON correctly. | ||
# Cast to str & load from JSON str before serializing | ||
rows = json.loads(str(rows)) | ||
with open("{{ output_filepath }}", "w") as file: | ||
json.dump(rows, file) |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -30,12 +30,25 @@ class _UnsupportedExtraUserRole(status_exception.StatusException): | |||||||||
|
||||||||||
def __init__(self, *, app_name: str, endpoint_name: str) -> None: | ||||||||||
message = ( | ||||||||||
f"{app_name} app requested unsupported extra user role on {endpoint_name} endpoint" | ||||||||||
f"{app_name} app requested unsupported extra user role on " | ||||||||||
f"{endpoint_name} endpoint" | ||||||||||
Comment on lines
-33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: why wrap? line length is 99 characters
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to make both exceptions have break lines in the same exact places. Easier to see where they differ (i.e. one includes the underlying exception message, while the other do not). |
||||||||||
) | ||||||||||
logger.warning(message) | ||||||||||
super().__init__(ops.BlockedStatus(message)) | ||||||||||
|
||||||||||
|
||||||||||
class _InvalidDatabaseName(status_exception.StatusException): | ||||||||||
"""Application charm requested an invalid database name""" | ||||||||||
|
||||||||||
def __init__(self, *, app_name: str, endpoint_name: str, exception_msg: str) -> None: | ||||||||||
message = ( | ||||||||||
f"{app_name} app requested an invalid database name on " | ||||||||||
f"{endpoint_name} endpoint: {exception_msg}" | ||||||||||
Comment on lines
+45
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
to keep message short (under 120 characters) so it doesn't get truncated in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather have a distinction between the log message (long format), and the message displayed when doing
|
||||||||||
) | ||||||||||
logger.warning(message) | ||||||||||
super().__init__(ops.BlockedStatus(exception_msg)) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Answered in this comment. |
||||||||||
|
||||||||||
|
||||||||||
class _Relation: | ||||||||||
"""Relation to one application charm""" | ||||||||||
|
||||||||||
|
@@ -79,9 +92,11 @@ def __init__( | |||||||||
if isinstance(event, ops.RelationBrokenEvent) and event.relation.id == self._id: | ||||||||||
raise _RelationBreaking | ||||||||||
self._database: str = self._databag["database"] | ||||||||||
self._app_name: str = relation.app.name | ||||||||||
self._endpoint_name: str = relation.name | ||||||||||
if self._databag.get("extra-user-roles"): | ||||||||||
raise _UnsupportedExtraUserRole( | ||||||||||
app_name=relation.app.name, endpoint_name=relation.name | ||||||||||
app_name=self._app_name, endpoint_name=self._endpoint_name | ||||||||||
) | ||||||||||
Comment on lines
97
to
100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you raise here if database name > 20 characters so that the _RelationThatRequestedUser object does not get instantiated? the current pattern/design is _RelationThatRequestedUser is only successfully initialized if the request is valid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered that approach too, to have the exception being raised in the Maybe exposing a new mysql_shell's |
||||||||||
|
||||||||||
def _set_databag( | ||||||||||
|
@@ -121,16 +136,24 @@ def create_database_and_user( | |||||||||
shell.delete_user(username, must_exist=False) | ||||||||||
logger.debug("Deleted user if exists before creating user") | ||||||||||
|
||||||||||
password = shell.create_application_database_and_user( | ||||||||||
username=username, database=self._database | ||||||||||
) | ||||||||||
|
||||||||||
self._set_databag( | ||||||||||
username=username, | ||||||||||
password=password, | ||||||||||
router_read_write_endpoints=router_read_write_endpoints, | ||||||||||
router_read_only_endpoints=router_read_only_endpoints, | ||||||||||
) | ||||||||||
try: | ||||||||||
password = shell.create_application_database( | ||||||||||
database=self._database, | ||||||||||
username=username, | ||||||||||
) | ||||||||||
except ValueError as exception: | ||||||||||
raise _InvalidDatabaseName( | ||||||||||
app_name=self._app_name, | ||||||||||
endpoint_name=self._endpoint_name, | ||||||||||
exception_msg=str(exception), | ||||||||||
) | ||||||||||
else: | ||||||||||
self._set_databag( | ||||||||||
username=username, | ||||||||||
password=password, | ||||||||||
router_read_write_endpoints=router_read_write_endpoints, | ||||||||||
router_read_only_endpoints=router_read_only_endpoints, | ||||||||||
) | ||||||||||
|
||||||||||
|
||||||||||
class _UserNotShared(Exception): | ||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.