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

feat[mysql_user]: add support for mysql user attributes #604

Merged
merged 6 commits into from
Jan 19, 2024

Conversation

n-cc
Copy link
Contributor

@n-cc n-cc commented Jan 10, 2024

SUMMARY

This PR adds support for MySQL user attributes (essentially arbitrary comments associated with a user in a key: value format) to the mysql_user module. It adds the ability to add, update, and delete user attributes, and get existing attributes by checking the attributes attribute of the mysql_user task.

ISSUE TYPE
  • Feature Pull Request
COMPONENT NAME

community.mysql.mysql_user

ADDITIONAL INFORMATION
  • Previously, user_mod would prematurely return at various points in check mode when changes were detected. IMO, this is not ideal, as the entirety of the module that will execute in non-check mode is not ran in check mode, and it means we have to support returning a specifically-constructed dict in multiple locations. As part of this PR, since I am modifying the return dict of user_mod, I removed the premature returns when in check mode in favor of the final return at the end of the function. I can revert this if desired, but I think it makes the code cleaner and safer. It is worth noting that messages from the module are still masked if a later message is registered.
  • Return values are not documented yet, so I didn't document the new attributes return value, but I could add return value documentation if desired.
LOCAL TESTING
SERVER SUPPORTS ATTRIBUTES

Creating a new user without attributes:

$ ansible -m mysql_user -a '{ "user": "newuser1", "host": "localhost" }' remote
remote | CHANGED => {
    "attributes": {},
    "changed": true,
    "msg": "User added",
    "password_changed": true,
    "user": "newuser1"
}
$ 

Creating a new user with attributes:

$ ansible -m mysql_user -a '{ "user": "newuser2", "host": "localhost", "attributes": { "key": "value" } }' remote
remote | CHANGED => {
    "attributes": {
        "key": "value"
    },
    "changed": true,
    "msg": "User added",
    "password_changed": true,
    "user": "newuser2"
}
$ 

Updating an existing user's attributes:

$ ansible -m mysql_user -a '{ "user": "newuser2", "host": "localhost", "attributes": { "key": "value_changed", "new_key": "new_value" } }' remote
remote | CHANGED => {
    "attributes": {
        "key": "value_changed",
        "new_key": "new_value"
    },
    "changed": true,
    "msg": "Attributes updated: key: value_changed, new_key: new_value",
    "password_changed": false,
    "user": "newuser2"
}
$ 

No change when existing attributes are already in line with the attributes specified in the attributes parameter:

$ ansible -m mysql_user -a '{ "user": "newuser2", "host": "localhost", "attributes": { "key": "value_changed", "new_key": "new_value" } }' remote
remote | SUCCESS => {
    "attributes": {
        "key": "value_changed",
        "new_key": "new_value"
    },
    "changed": false,
    "msg": "User unchanged",
    "password_changed": false,
    "user": "newuser2"
}
$ 

No change when the attributes parameter is not specified:

$ ansible -m mysql_user -a '{ "user": "newuser2", "host": "localhost" }' remote
remote | SUCCESS => {
    "attributes": {
        "key": "value_changed",
        "new_key": "new_value"
    },
    "changed": false,
    "msg": "User unchanged",
    "password_changed": false,
    "user": "newuser2"
}
$ 
SERVER DOES NOT SUPPORT ATTRIBUTES

Creating a new user without attributes:

$ ansible -m mysql_user -a '{ "user": "newuser1", "host": "localhost" }' remote
remote | CHANGED => {
    "attributes": {},
    "changed": true,
    "msg": "User added",
    "password_changed": true,
    "user": "newuser1"
}
$ 

Reading an existing user:

$ ansible -m mysql_user -a '{ "user": "newuser1", "host": "localhost" }' remote
remote | SUCCESS => {
    "attributes": {},
    "changed": false,
    "msg": "User unchanged",
    "password_changed": false,
    "user": "newuser1"
}
$ 

Creating a new user with attributes:

$ ansible -m mysql_user -a '{ "user": "newuser2", "host": "localhost", "attributes": { "key": "value" } }' remote
remote | FAILED! => {
    "changed": false,
    "msg": "user attributes were specified but the mysql server does not support user attributes"
}
$ 

Updating an existing user's attributes:

$ ansible -m mysql_user -a '{ "user": "newuser1", "host": "localhost", "attributes": { "key": "value" } }' remote
remote | FAILED! => {
    "changed": false,
    "msg": "user attributes were specified but the mysql server does not support user attributes"
}
$ 

- MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0.
- To delete an existing attribute, set its value to False.
type: dict
version_added: '3.9.0'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not bump the version in galaxy.yml yet; unsure what the process is for shipping new versions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

3.9.0 is OK here

@@ -405,6 +418,7 @@ def main():
tls_requires=dict(type='dict'),
append_privs=dict(type='bool', default=False),
subtract_privs=dict(type='bool', default=False),
attributes=dict(type='dict'),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can change the parameter ordering here if desired.

Copy link
Collaborator

Choose a reason for hiding this comment

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

that's fine now

@n-cc n-cc force-pushed the ncc-user-attributes branch 2 times, most recently from 4e146a2 to 8685d4a Compare January 10, 2024 22:25
@n-cc
Copy link
Contributor Author

n-cc commented Jan 10, 2024

I'll work on resolving the lint and integration tests over the next day or so.

Copy link

codecov bot commented Jan 11, 2024

Codecov Report

Attention: 18 lines in your changes are missing coverage. Please review.

Comparison is base (81ab18d) 76.66% compared to head (208719f) 75.13%.

Files Patch % Lines
plugins/module_utils/user.py 77.50% 12 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #604      +/-   ##
==========================================
- Coverage   76.66%   75.13%   -1.53%     
==========================================
  Files          28       18      -10     
  Lines        2447     2361      -86     
  Branches      603      595       -8     
==========================================
- Hits         1876     1774     -102     
- Misses        388      405      +17     
+ Partials      183      182       -1     
Flag Coverage Δ
integration 74.50% <77.64%> (+0.33%) ⬆️
sanity ?
units ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@n-cc
Copy link
Contributor Author

n-cc commented Jan 11, 2024

Existing integration tests are passing, going to work on writing new ones for this feature.

@n-cc n-cc force-pushed the ncc-user-attributes branch 8 times, most recently from 3467950 to 0e56260 Compare January 11, 2024 22:55
@n-cc
Copy link
Contributor Author

n-cc commented Jan 11, 2024

Integration tests written, everything should be good for a review at this point.

Copy link
Collaborator

@Andersson007 Andersson007 left a comment

Choose a reason for hiding this comment

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

@n-cc hello, thanks for the PR!
could you please add a changelog fragment?
Also will you be responsive in case of related bug reports? If not really, should we implement it somehow separately from all those functions you're changing here? But i hope you're going to be around and even join our Matrix channel and Forum group:) See the communication guide.

plugins/modules/mysql_user.py Outdated Show resolved Hide resolved
plugins/modules/mysql_user.py Outdated Show resolved Hide resolved
Comment on lines 1004 to 1012
if r:
attributes = r[0]
# convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format
if attributes:
return json.loads(attributes)
else:
return {}

return None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if r:
attributes = r[0]
# convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format
if attributes:
return json.loads(attributes)
else:
return {}
return None
# convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format
return json.loads(r[0]) if r and r[0] else None

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this will return None rather than an empty dict if the user exists but does not have any attributes set - are you recommending returning None over {} in that scenario?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yep

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That should be fine, but there are a few things to keep in mind about None vs {} when it comes to attributes... if a CREATE USER is ran without the ATTRIBUTES '{}' option, the user's attributes are stored as NULL until attributes are set, and if all attributes are deleted, attributes will then be stored as {} and cannot be changed back to NULL (as far as I can tell):

mysql> CREATE USER 'testuser'@'localhost';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT ATTRIBUTE from INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = 'testuser' and host = 'localhost';
+-----------+
| ATTRIBUTE |
+-----------+
| NULL      |
+-----------+
1 row in set (0.00 sec)

mysql> ALTER USER 'testuser'@'localhost' ATTRIBUTE '{ "foo": "bar" }';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT ATTRIBUTE from INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = 'testuser' and host = 'localhost';
+----------------+
| ATTRIBUTE      |
+----------------+
| {"foo": "bar"} |
+----------------+
1 row in set (0.00 sec)

mysql> ALTER USER 'testuser'@'localhost' ATTRIBUTE '{ "foo": null }';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT ATTRIBUTE from INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = 'testuser' and host = 'localhost';
+-----------+
| ATTRIBUTE |
+-----------+
| {}        |
+-----------+
1 row in set (0.00 sec)

mysql>

I think there are a couple different options:

  1. Represent no attributes as {} even if the ATTRIBUTE database value is NULL
    1. This is what I was doing originally; I chose this method so that someone using register: foo with mysql_user could still treat foo.attributes as a dictionary even if the ATTRIBUTE value is NULL. It also makes the code a bit cleaner if attributes_get always returns a dict for users that exist.
    2. With this method, we could also explicitly run the CREATE USER query in user_add with ATTRIBUTES {} even if no attributes are specified, so that the ATTRIBUTE database value will always be {} for ansible-created users on servers with attribute support.
  2. Represent no attributes as None even if the ATTRIBUTE database value is {} (I think this is what you were proposing)
  3. Represent no attributes as either None or {} depending on the database value

Option 2 is how this feature behaves as of my latest commit (I also wrote tests to assert the attributes return value for users without attributes). I think any of these options are fine depending on ansible best practices. I can stick with option 2 if you think it's preferred, I just wanted to make sure this caveat was documented in this PR.

- MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0.
- To delete an existing attribute, set its value to False.
type: dict
version_added: '3.9.0'
Copy link
Collaborator

Choose a reason for hiding this comment

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

3.9.0 is OK here

@@ -405,6 +418,7 @@ def main():
tls_requires=dict(type='dict'),
append_privs=dict(type='bool', default=False),
subtract_privs=dict(type='bool', default=False),
attributes=dict(type='dict'),
Copy link
Collaborator

Choose a reason for hiding this comment

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

that's fine now

plugins/module_utils/user.py Outdated Show resolved Hide resolved
@n-cc
Copy link
Contributor Author

n-cc commented Jan 12, 2024

Also will you be responsive in case of related bug reports?

Certainly if there are any immediate problems, but you could also consider me a maintainer of this feature if future problems arise. I don't actively use irc/matrix, but I should be reachable via Github pings.

That being said, if implementing attribute support separately from user_mod and user_add is preferable, I can do that, I just opted to include everything in user_mod as it seems like that's where all user modification is handled currently.

@n-cc n-cc force-pushed the ncc-user-attributes branch 2 times, most recently from 0119896 to 9eb4cc3 Compare January 16, 2024 19:55
@n-cc
Copy link
Contributor Author

n-cc commented Jan 16, 2024

Requested changes have been implemented, aside from the two I had questions on.

@Andersson007
Copy link
Collaborator

Also will you be responsive in case of related bug reports?

Certainly if there are any immediate problems, but you could also consider me a maintainer of this feature if future problems arise. I don't actively use irc/matrix, but I should be reachable via Github pings.

That being said, if implementing attribute support separately from user_mod and user_add is preferable, I can do that, I just opted to include everything in user_mod as it seems like that's where all user modification is handled currently.

that's nice to hear! Could you please take a look at the comment about using None. thanks

@n-cc n-cc force-pushed the ncc-user-attributes branch 6 times, most recently from 6c4c394 to b84d8fb Compare January 17, 2024 21:01
@n-cc
Copy link
Contributor Author

n-cc commented Jan 17, 2024

Also will you be responsive in case of related bug reports?

Certainly if there are any immediate problems, but you could also consider me a maintainer of this feature if future problems arise. I don't actively use irc/matrix, but I should be reachable via Github pings.
That being said, if implementing attribute support separately from user_mod and user_add is preferable, I can do that, I just opted to include everything in user_mod as it seems like that's where all user modification is handled currently.

that's nice to hear! Could you please take a look at the comment about using None. thanks

Responded and updated.

Copy link
Collaborator

@Andersson007 Andersson007 left a comment

Choose a reason for hiding this comment

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

@n-cc one small thing to change, the rest LGTM

CHANGELOG.rst Outdated Show resolved Hide resolved
@n-cc
Copy link
Contributor Author

n-cc commented Jan 18, 2024

@n-cc one small thing to change, the rest LGTM

Thanks much, updated

@Andersson007
Copy link
Collaborator

@laurent-indermuehle is it OK for you? if yes, please merge

@Andersson007
Copy link
Collaborator

as @laurent-indermuehle is busy, I'm merging this PR

@n-cc thanks for the contribution!
@laurent-indermuehle thanks for reviewing!

@Andersson007 Andersson007 merged commit 051aa48 into ansible-collections:main Jan 19, 2024
40 of 41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants