diff --git a/db/databases/operations/drop.py b/db/databases/operations/drop.py new file mode 100644 index 0000000000..1d58aa4e4d --- /dev/null +++ b/db/databases/operations/drop.py @@ -0,0 +1,14 @@ +from db.connection import exec_msar_func +from psycopg import sql + + +def drop_database(database_oid, conn): + cursor = conn.cursor() + conn.autocommit = True + drop_database_query = exec_msar_func( + conn, + 'drop_database_query', + database_oid + ).fetchone()[0] + cursor.execute(sql.SQL(drop_database_query)) + cursor.close() diff --git a/db/roles/operations/drop.py b/db/roles/operations/drop.py new file mode 100644 index 0000000000..6c426bcbba --- /dev/null +++ b/db/roles/operations/drop.py @@ -0,0 +1,5 @@ +from db.connection import exec_msar_func + + +def drop_role(role_oid, conn): + exec_msar_func(conn, 'drop_role', role_oid) diff --git a/db/sql/00_msar.sql b/db/sql/00_msar.sql index fa902eb064..4e85cc69c7 100644 --- a/db/sql/00_msar.sql +++ b/db/sql/00_msar.sql @@ -516,6 +516,28 @@ END; $$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; +CREATE OR REPLACE FUNCTION msar.get_database_name(dat_id oid) RETURNS TEXT AS $$/* +Return the UNQUOTED name of a given database. + +If the database does not exist, an exception will be raised. + +Args: + dat_id: The OID of the role. +*/ +DECLARE dat_name text; +BEGIN + SELECT datname INTO dat_name FROM pg_catalog.pg_database WHERE oid=dat_id; + + IF dat_name IS NULL THEN + RAISE EXCEPTION 'Database with OID % does not exist', dat_id + USING ERRCODE = '42704'; -- undefined_object + END IF; + + RETURN dat_name; +END; +$$ LANGUAGE plpgsql STABLE RETURNS NULL ON NULL INPUT; + + CREATE OR REPLACE FUNCTION msar.get_role_name(rol_oid oid) RETURNS TEXT AS $$/* Return the UNQUOTED name of a given role. @@ -526,7 +548,7 @@ Args: */ DECLARE rol_name text; BEGIN - SELECT rolname INTO rol_name FROM pg_roles WHERE oid=rol_oid; + SELECT rolname INTO rol_name FROM pg_catalog.pg_roles WHERE oid=rol_oid; IF rol_name IS NULL THEN RAISE EXCEPTION 'Role with OID % does not exist', rol_oid @@ -535,7 +557,7 @@ BEGIN RETURN rol_name; END; -$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; +$$ LANGUAGE plpgsql STABLE RETURNS NULL ON NULL INPUT; CREATE OR REPLACE FUNCTION msar.get_constraint_type_api_code(contype char) RETURNS TEXT AS $$/* @@ -1326,6 +1348,23 @@ END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION +msar.drop_role(rol_id regrole) RETURNS void AS $$/* +Drop a role. + +Note: +- To drop a superuser role, you must be a superuser yourself. +- To drop non-superuser roles, you must have CREATEROLE privilege and have been granted ADMIN OPTION on the role. + +Args: + rol_id: The OID of the role to drop on the database. +*/ +BEGIN + EXECUTE format('DROP ROLE %I', msar.get_role_name(rol_id)); +END; +$$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION msar.build_database_privilege_replace_expr(rol_id regrole, privileges_ jsonb) RETURNS TEXT AS $$ SELECT string_agg( @@ -1680,6 +1719,43 @@ END; $$ LANGUAGE plpgsql; +---------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- +-- DROP DATABASE FUNCTIONS +-- +-- Drop a database. +---------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + + +CREATE OR REPLACE FUNCTION +msar.drop_database_query(dat_id oid) RETURNS text AS $$/* +Return the SQL query to drop a database. + +If no database exists with the given oid, an exception will be raised. + +Args: + dat_id: The OID of the role to drop. +*/ +BEGIN + RETURN format('DROP DATABASE %I', msar.get_database_name(dat_id)); +END; +$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; + + +CREATE OR REPLACE FUNCTION +msar.drop_database_query(dat_name text) RETURNS text AS $$/* +Return the SQL query to drop a database. + +Args: + dat_id: An unqoted name of the database to be dropped. +*/ +BEGIN + RETURN format('DROP DATABASE %I', dat_name); +END; +$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; + + ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- -- DROP SCHEMA FUNCTIONS diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md index 6933ca652e..bc88e9f725 100644 --- a/docs/docs/api/rpc.md +++ b/docs/docs/api/rpc.md @@ -64,6 +64,7 @@ To use an RPC function: options: members: - get + - delete - DatabaseInfo ## Configured Databases @@ -72,6 +73,7 @@ To use an RPC function: options: members: - list_ + - disconnect - ConfiguredDatabaseInfo ## Database Privileges @@ -252,6 +254,7 @@ To use an RPC function: members: - list_ - add + - delete - get_current_role - RoleInfo - RoleMember diff --git a/mathesar/rpc/databases/base.py b/mathesar/rpc/databases/base.py index da2968a447..fa68060e34 100644 --- a/mathesar/rpc/databases/base.py +++ b/mathesar/rpc/databases/base.py @@ -5,6 +5,7 @@ from mathesar.rpc.utils import connect from db.databases.operations.select import get_database +from db.databases.operations.drop import drop_database from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions @@ -53,3 +54,19 @@ def get(*, database_id: int, **kwargs) -> DatabaseInfo: with connect(database_id, user) as conn: db_info = get_database(conn) return DatabaseInfo.from_dict(db_info) + + +@rpc_method(name="databases.delete") +@http_basic_auth_login_required +@handle_rpc_exceptions +def delete(*, database_oid: int, database_id: int, **kwargs) -> None: + """ + Drop a database from the server. + + Args: + database_oid: The OID of the database to delete on the database. + database_id: The Django id of the database to connect to. + """ + user = kwargs.get(REQUEST_KEY).user + with connect(database_id, user) as conn: + drop_database(database_oid, conn) diff --git a/mathesar/rpc/databases/configured.py b/mathesar/rpc/databases/configured.py index f22ca281e3..4758785d66 100644 --- a/mathesar/rpc/databases/configured.py +++ b/mathesar/rpc/databases/configured.py @@ -50,3 +50,17 @@ def list_(*, server_id: int = None, **kwargs) -> list[ConfiguredDatabaseInfo]: database_qs = Database.objects.all() return [ConfiguredDatabaseInfo.from_model(db_model) for db_model in database_qs] + + +@rpc_method(name="databases.configured.disconnect") +@http_basic_auth_login_required +@handle_rpc_exceptions +def disconnect(*, database_id: int, **kwargs) -> None: + """ + Disconnect a configured database. + + Args: + database_id: The Django id of the database. + """ + database_qs = Database.objects.get(id=database_id) + database_qs.delete() diff --git a/mathesar/rpc/roles/base.py b/mathesar/rpc/roles/base.py index 9eb0de8402..ce1f7d3e8a 100644 --- a/mathesar/rpc/roles/base.py +++ b/mathesar/rpc/roles/base.py @@ -10,6 +10,7 @@ from mathesar.rpc.utils import connect 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 class RoleMember(TypedDict): @@ -118,6 +119,27 @@ def add( return RoleInfo.from_dict(role) +@rpc_method(name="roles.delete") +@http_basic_auth_login_required +@handle_rpc_exceptions +def delete( + *, + role_oid: int, + database_id: int, + **kwargs +) -> None: + """ + Drop a role on a database server. + + Args: + role_oid: The OID of the role to drop on the database. + database_id: The Django id of the database. + """ + user = kwargs.get(REQUEST_KEY).user + with connect(database_id, user) as conn: + drop_role(role_oid, conn) + + @rpc_method(name="roles.get_current_role") @http_basic_auth_login_required @handle_rpc_exceptions diff --git a/mathesar/tests/rpc/test_endpoints.py b/mathesar/tests/rpc/test_endpoints.py index 218d4ebeb8..3a8f5a38d0 100644 --- a/mathesar/tests/rpc/test_endpoints.py +++ b/mathesar/tests/rpc/test_endpoints.py @@ -144,12 +144,22 @@ "databases.get", [user_is_authenticated] ), + ( + databases.delete, + "databases.delete", + [user_is_authenticated] + ), ( databases.configured.list_, "databases.configured.list", [user_is_authenticated] ), + ( + databases.configured.disconnect, + "databases.configured.disconnect", + [user_is_authenticated] + ), ( databases.privileges.list_direct, @@ -255,6 +265,11 @@ "roles.add", [user_is_authenticated] ), + ( + roles.delete, + "roles.delete", + [user_is_authenticated] + ), ( roles.get_current_role, "roles.get_current_role", diff --git a/mathesar/tests/rpc/test_roles.py b/mathesar/tests/rpc/test_roles.py index 7d9cbe539f..35b82cdd17 100644 --- a/mathesar/tests/rpc/test_roles.py +++ b/mathesar/tests/rpc/test_roles.py @@ -110,6 +110,36 @@ def mock_create_role(rolename, password, login, conn): roles.add(rolename=_username, database_id=_database_id, password=_password, login=True, request=request) +def test_roles_delete(rf, monkeypatch): + _username = 'alice' + _password = 'pass1234' + _database_id = 2 + _role_oid = 10 + 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_drop_role(role_oid, conn): + if ( + role_oid != _role_oid + ): + raise AssertionError('incorrect parameters passed') + return None + + monkeypatch.setattr(roles.base, 'connect', mock_connect) + monkeypatch.setattr(roles.base, 'drop_role', mock_drop_role) + roles.delete(role_oid=_role_oid, database_id=_database_id, request=request) + + def test_get_current_role(rf, monkeypatch): _username = 'alice' _password = 'pass1234'