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

Implement roles.set_members RPC endpoint #3866

Merged
merged 7 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions db/roles/operations/membership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from db.connection import exec_msar_func


def set_members_to_role(parent_role_oid, members, conn):
return exec_msar_func(conn, 'set_members_to_role', parent_role_oid, members).fetchone()[0]
86 changes: 86 additions & 0 deletions db/sql/00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,92 @@ WHERE role_data.name = rolename;
$$ LANGUAGE SQL STABLE;


CREATE OR REPLACE FUNCTION
msar.build_grant_membership_expr(parent_rol_id regrole, g_roles oid[]) RETURNS TEXT AS $$
SELECT string_agg(
format(
'GRANT %1$s TO %2$s',
msar.get_role_name(parent_rol_id),
msar.get_role_name(rol_id)
),
E';\n'
) || E';\n'
FROM unnest(g_roles) as x(rol_id);
$$ LANGUAGE SQL STABLE RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION
msar.build_revoke_membership_expr(parent_rol_id regrole, r_roles oid[]) RETURNS TEXT AS $$
SELECT string_agg(
format(
'REVOKE %1$s FROM %2$s',
msar.get_role_name(parent_rol_id),
msar.get_role_name(rol_id)
),
E';\n'
) || E';\n'
FROM unnest(r_roles) as x(rol_id);
$$ LANGUAGE SQL STABLE RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION msar.set_members_to_role(parent_rol_id regrole, members oid[]) RETURNS jsonb AS $$/*
Grant/Revoke direct membership to/from roles.

Returns a json object describing the updated information of the parent role.

{
"oid": <int>
"name": <str>
"super": <bool>
"inherits": <bool>
"create_role": <bool>
"create_db": <bool>
"login": <bool>
"description": <str|null>
"members": <[
{ "oid": <int>, "admin": <bool> }
]|null>
}

Args:
parent_rol_id: The OID of role whose membership will be granted/revoked to/from other roles.
members: An array of role OID(s) whom we want to grant direct membership of the parent role.
Only the OID(s) present in the array will be granted membership of parent role,
Membership will be revoked for existing members not present in this array.
*/
DECLARE
parent_role_info jsonb := msar.get_role(parent_rol_id::regrole::text);
all_members_array bigint[];
revoke_members_array bigint[];
set_members_expr text;
BEGIN
-- Get all the members of parent_role.
SELECT array_agg(x.oid)
FROM jsonb_to_recordset(
CASE WHEN parent_role_info ->> 'members' IS NOT NULL
THEN parent_role_info -> 'members'
ELSE NULL END
) AS x(oid oid, admin boolean)
INTO all_members_array;
-- Find all the roles whose membership we want to revoke.
SELECT ARRAY(
SELECT unnest(all_members_array)
EXCEPT
SELECT unnest(members)
) INTO revoke_members_array;
-- REVOKE/GRANT membership for parent_role.
set_members_expr := concat_ws(
E'\n',
msar.build_revoke_membership_expr(parent_rol_id, revoke_members_array),
msar.build_grant_membership_expr(parent_rol_id, members)
);
EXECUTE set_members_expr;
-- Return the updated parent_role info including membership details.
RETURN msar.get_role(parent_rol_id::regrole::text);
END;
$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION
msar.get_current_role() RETURNS jsonb AS $$/*
Returns a JSON object describing the current_role and the parent role(s) whose
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ To use an RPC function:
- add
- delete
- get_current_role
- set_members
- RoleInfo
- RoleMember

Expand Down
29 changes: 29 additions & 0 deletions mathesar/rpc/roles/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from db.roles.operations.select import list_roles, get_current_role_from_db
from db.roles.operations.create import create_role
from db.roles.operations.drop import drop_role
from db.roles.operations.membership import set_members_to_role


class RoleMember(TypedDict):
Expand Down Expand Up @@ -161,3 +162,31 @@ def get_current_role(*, database_id: int, **kwargs) -> dict:
"current_role": RoleInfo.from_dict(current_role["current_role"]),
"parent_roles": [RoleInfo.from_dict(role) for role in current_role["parent_roles"]]
}


@rpc_method(name="roles.set_members")
@http_basic_auth_login_required
@handle_rpc_exceptions
def set_members(
*,
parent_role_oid: int,
members: list,
database_id: int,
**kwargs
) -> RoleInfo:
"""
Grant/Revoke direct membership to/from roles.

Args:
parent_role_oid: The OID of role whose membership will be granted/revoked to/from other roles.
members: An array of role OID(s) whom we want to grant direct membership of the parent role.
Only the OID(s) present in the array will be granted membership of parent role,
Membership will be revoked for existing members not present in this array.

Returns:
A dict describing the updated information of the parent role.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
parent_role = set_members_to_role(parent_role_oid, members, conn)
return RoleInfo.from_dict(parent_role)
5 changes: 5 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@
"roles.get_current_role",
[user_is_authenticated]
),
(
roles.set_members,
"roles.set_members",
[user_is_authenticated]
),

(
roles.configured.add,
Expand Down
42 changes: 42 additions & 0 deletions mathesar/tests/rpc/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,45 @@ def mock_get_current_role(conn):
monkeypatch.setattr(roles.base, 'connect', mock_connect)
monkeypatch.setattr(roles.base, 'get_current_role_from_db', mock_get_current_role)
roles.get_current_role(database_id=_database_id, request=request)


def test_roles_set_members(rf, monkeypatch):
_username = 'alice'
_password = 'pass1234'
_database_id = 2
_parent_role_oid = 10
_members = [2573031, 2573040]
request = rf.post('/api/rpc/v0/', data={})
request.user = User(username=_username, password=_password)

@contextmanager
def mock_connect(database_id, user):
if database_id == _database_id and user.username == _username:
try:
yield True
finally:
pass
else:
raise AssertionError('incorrect parameters passed')

def mock_set_roles(parent_role_oid, members, conn):
if (
parent_role_oid != _parent_role_oid
or members != _members
):
raise AssertionError('incorrect parameters passed')
return {
'oid': 10,
'name': 'mathesar',
'login': True,
'super': True,
'members': [{'oid': 2573031, 'admin': False}, {'oid': 2573040, 'admin': False}],
'inherits': True,
'create_db': True,
'create_role': True,
'description': None
}

monkeypatch.setattr(roles.base, 'connect', mock_connect)
monkeypatch.setattr(roles.base, 'set_members_to_role', mock_set_roles)
roles.set_members(parent_role_oid=_parent_role_oid, database_id=_database_id, members=_members, request=request)
2 changes: 1 addition & 1 deletion mathesar_ui/src/api/rpc/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const roles = {
set_members: rpcMethodTypeContainer<
{
database_id: RawDatabase['id'];
role_oid: RawRole['oid'];
parent_role_oid: RawRole['oid'];
members: RawRole['oid'][];
},
RawRole
Expand Down
2 changes: 1 addition & 1 deletion mathesar_ui/src/models/Role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class Role {
const promise = api.roles
.set_members({
database_id: this.database.id,
role_oid: this.oid,
parent_role_oid: this.oid,
members: [...memberOids],
})
.run();
Expand Down
Loading