mirror of
https://github.com/timescale/timescaledb.git
synced 2025-05-17 11:03:36 +08:00
Allow non-superusers to work with the db (but also mess up the catalog)
Previous to this commit non-superusers could not do anything inside a database with the timescale extension loaded. Now, non-superuser can create their own hypertables and work inside the db. There are two big caveats: 1) All users have read/write permissions to the timescaledb catalog. 2) Permission changes applied to the main tables are not propagated to the associated tables.
This commit is contained in:
parent
21d4ebb23b
commit
fbdcab186f
@ -193,13 +193,13 @@ this creates a more compact, and thus efficient, index.
|
||||
Below are a few current limitations of our database, which we are
|
||||
actively working to resolve:
|
||||
|
||||
- Anyone using TimescaleDB currently needs superuser privileges (this is
|
||||
getting fixed very soon)
|
||||
- Any user has full read/write access to the metadata tables for hypertables.
|
||||
- Permission changes on hypertables are not correctly propagated.
|
||||
- `create_hypertable()` can only be run on an empty table
|
||||
- `COPY`ing a dataset will currently put all data in the same chunk, even if
|
||||
chunk size goes over max size. For now we recommend breaking down large
|
||||
files for `COPY` (e.g., large CSVs) into smaller files that are slightly
|
||||
larger than max_chunk size (currently 1GB).
|
||||
larger than max_chunk size (currently 1GB by default).
|
||||
We provide `scripts/migrate_data.sh` to help with this.
|
||||
- Custom user-created triggers on hypertables currently will not work (i.e.,
|
||||
fire)
|
||||
|
37
sql/common/permissions.sql
Normal file
37
sql/common/permissions.sql
Normal file
@ -0,0 +1,37 @@
|
||||
--schema permisions
|
||||
GRANT USAGE ON SCHEMA _timescaledb_catalog, _timescaledb_meta, _timescaledb_meta_api, _timescaledb_data_api, _timescaledb_cache
|
||||
TO PUBLIC;
|
||||
|
||||
GRANT USAGE, CREATE ON SCHEMA _timescaledb_internal TO PUBLIC;
|
||||
|
||||
--needed for working with hypertables
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA _timescaledb_catalog TO PUBLIC;
|
||||
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA _timescaledb_catalog TO PUBLIC;
|
||||
|
||||
--Needed but dangerous. Anybody can mess up the _timescaledb_catalog.
|
||||
--MUST DOCUMENT TODO: remove these permissions. Have c-based workaround.
|
||||
--Everything below this line is suspect.
|
||||
GRANT INSERT ON TABLE _timescaledb_catalog.hypertable,
|
||||
_timescaledb_catalog.default_replica_node, _timescaledb_catalog.hypertable_replica, _timescaledb_catalog.partition_epoch,
|
||||
_timescaledb_catalog.partition, _timescaledb_catalog.partition_replica,
|
||||
_timescaledb_catalog.hypertable_column,
|
||||
_timescaledb_catalog.chunk, _timescaledb_catalog.chunk_replica_node
|
||||
TO PUBLIC;
|
||||
|
||||
--needed for inserts to hypertable
|
||||
GRANT UPDATE ON TABLE _timescaledb_catalog.hypertable, _timescaledb_catalog.chunk,
|
||||
_timescaledb_catalog.partition, _timescaledb_catalog.partition_epoch --needed for lock
|
||||
TO PUBLIC;
|
||||
|
||||
--needed for ddl
|
||||
GRANT INSERT, DELETE ON TABLE _timescaledb_catalog.hypertable_index, _timescaledb_catalog.chunk_replica_node_index
|
||||
TO PUBLIC;
|
||||
|
||||
GRANT DELETE, UPDATE ON TABLE _timescaledb_catalog.hypertable_column
|
||||
TO PUBLIC;
|
||||
|
||||
GRANT INSERT ON TABLE _timescaledb_catalog.deleted_hypertable_index, _timescaledb_catalog.deleted_hypertable_column
|
||||
TO PUBLIC;
|
||||
|
||||
|
@ -47,3 +47,4 @@ sql/main/ddl_util.sql
|
||||
sql/main/ddl.sql
|
||||
sql/main/ddl_triggers.sql
|
||||
sql/main/setup_main.sql
|
||||
sql/common/permissions.sql
|
||||
|
@ -43,10 +43,22 @@ DECLARE
|
||||
remote_node _timescaledb_catalog.node;
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
DECLARE
|
||||
cnt INTEGER;
|
||||
BEGIN
|
||||
EXECUTE format(
|
||||
$$
|
||||
CREATE SCHEMA IF NOT EXISTS %I
|
||||
$$, NEW.associated_schema_name);
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN
|
||||
SELECT COUNT(*) INTO cnt
|
||||
FROM pg_namespace
|
||||
WHERE nspname = NEW.associated_schema_name;
|
||||
IF cnt = 0 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
|
||||
PERFORM _timescaledb_internal.create_table(NEW.root_schema_name, NEW.root_table_name);
|
||||
|
||||
|
@ -66,21 +66,11 @@ static Catalog catalog = {
|
||||
Catalog *
|
||||
catalog_get(void)
|
||||
{
|
||||
AclResult aclresult;
|
||||
int i;
|
||||
|
||||
if (MyDatabaseId == InvalidOid)
|
||||
elog(ERROR, "Invalid database ID");
|
||||
|
||||
/*
|
||||
* Check that the user has CREATE permissions on the database, since the
|
||||
* operation may involve creating chunks and inserting into them.
|
||||
*/
|
||||
aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
|
||||
|
||||
if (aclresult != ACLCHECK_OK)
|
||||
aclcheck_error(aclresult, ACL_KIND_DATABASE,
|
||||
get_database_name(MyDatabaseId));
|
||||
|
||||
if (MyDatabaseId == catalog.database_id)
|
||||
return &catalog;
|
||||
|
213
test/expected/alternate_users.out
Normal file
213
test/expected/alternate_users.out
Normal file
@ -0,0 +1,213 @@
|
||||
\set ON_ERROR_STOP 1
|
||||
\set VERBOSITY verbose
|
||||
\set SHOW_CONTEXT never
|
||||
\set ECHO ALL
|
||||
\ir include/insert_single.sql
|
||||
\set ON_ERROR_STOP 1
|
||||
\ir create_single_db.sql
|
||||
\set VERBOSITY default
|
||||
SET client_min_messages = WARNING;
|
||||
DROP DATABASE IF EXISTS single;
|
||||
SET client_min_messages = NOTICE;
|
||||
CREATE DATABASE single;
|
||||
\c single
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
|
||||
psql:include/create_single_db.sql:8: NOTICE: installing required extension "dblink"
|
||||
psql:include/create_single_db.sql:8: NOTICE: installing required extension "postgres_fdw"
|
||||
psql:include/create_single_db.sql:8: NOTICE: installing required extension "hstore"
|
||||
SELECT setup_db(hostname => 'fakehost'); -- fakehost makes sure there is no network connection
|
||||
setup_db
|
||||
----------
|
||||
|
||||
(1 row)
|
||||
|
||||
\set VERBOSITY verbose
|
||||
\c single
|
||||
\set ECHO ALL
|
||||
CREATE TABLE PUBLIC."testNs" (
|
||||
"timeCustom" BIGINT NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
series_0 DOUBLE PRECISION NULL,
|
||||
series_1 DOUBLE PRECISION NULL,
|
||||
series_2 DOUBLE PRECISION NULL,
|
||||
series_bool BOOLEAN NULL
|
||||
);
|
||||
CREATE INDEX ON PUBLIC."testNs" (device_id, "timeCustom" DESC NULLS LAST) WHERE device_id IS NOT NULL;
|
||||
CREATE INDEX ON PUBLIC."testNs" ("timeCustom" DESC NULLS LAST, series_0) WHERE series_0 IS NOT NULL;
|
||||
CREATE INDEX ON PUBLIC."testNs" ("timeCustom" DESC NULLS LAST, series_1) WHERE series_1 IS NOT NULL;
|
||||
CREATE INDEX ON PUBLIC."testNs" ("timeCustom" DESC NULLS LAST, series_2) WHERE series_2 IS NOT NULL;
|
||||
CREATE INDEX ON PUBLIC."testNs" ("timeCustom" DESC NULLS LAST, series_bool) WHERE series_bool IS NOT NULL;
|
||||
SELECT * FROM create_hypertable('"public"."testNs"', 'timeCustom', associated_schema_name=>'testNs' );
|
||||
create_hypertable
|
||||
-------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
BEGIN;
|
||||
\COPY "testNs" FROM 'data/ds1_dev1_1.tsv' NULL AS '';
|
||||
COMMIT;
|
||||
SELECT _timescaledb_meta_api.close_chunk_end_immediate(c.id)
|
||||
FROM get_open_partition_for_key((SELECT id FROM _timescaledb_catalog.hypertable WHERE table_name = 'testNs'), 'dev1') part
|
||||
INNER JOIN _timescaledb_catalog.chunk c ON (c.partition_id = part.id);
|
||||
close_chunk_end_immediate
|
||||
---------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
INSERT INTO "testNs"("timeCustom", device_id, series_0, series_1) VALUES
|
||||
(1257987600000000000, 'dev1', 1.5, 1),
|
||||
(1257987600000000000, 'dev1', 1.5, 2),
|
||||
(1257894000000000000, 'dev20', 1.5, 1),
|
||||
(1257894002000000000, 'dev1', 2.5, 3);
|
||||
INSERT INTO "testNs"("timeCustom", device_id, series_0, series_1) VALUES
|
||||
(1257894000000000000, 'dev20', 1.5, 2);
|
||||
CREATE TABLE chunk_closing_test(
|
||||
time BIGINT,
|
||||
metric INTEGER,
|
||||
device_id TEXT
|
||||
);
|
||||
-- Test chunk closing/creation
|
||||
SELECT * FROM create_hypertable('chunk_closing_test', 'time', chunk_size_bytes => 10000);
|
||||
create_hypertable
|
||||
-------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
INSERT INTO chunk_closing_test VALUES(1, 1, 'dev1');
|
||||
INSERT INTO chunk_closing_test VALUES(2, 2, 'dev2');
|
||||
INSERT INTO chunk_closing_test VALUES(3, 3, 'dev3');
|
||||
SELECT * FROM chunk_closing_test;
|
||||
time | metric | device_id
|
||||
------+--------+-----------
|
||||
1 | 1 | dev1
|
||||
2 | 2 | dev2
|
||||
3 | 3 | dev3
|
||||
(3 rows)
|
||||
|
||||
SELECT * FROM _timescaledb_catalog.chunk c
|
||||
LEFT JOIN _timescaledb_catalog.chunk_replica_node crn ON (c.id = crn.chunk_id)
|
||||
LEFT JOIN _timescaledb_catalog.partition_replica pr ON (crn.partition_replica_id = pr.id)
|
||||
LEFT JOIN _timescaledb_catalog.hypertable h ON (pr.hypertable_id = h.id)
|
||||
WHERE h.schema_name = 'public' AND h.table_name = 'chunk_closing_test';
|
||||
id | partition_id | start_time | end_time | chunk_id | partition_replica_id | database_name | schema_name | table_name | id | partition_id | hypertable_id | replica_id | schema_name | table_name | id | schema_name | table_name | associated_schema_name | associated_table_prefix | root_schema_name | root_table_name | replication_factor | placement | time_column_name | time_column_type | created_on | chunk_size_bytes
|
||||
----+--------------+------------+----------+----------+----------------------+---------------+-----------------------+---------------------+----+--------------+---------------+------------+-----------------------+------------------------+----+-------------+--------------------+------------------------+-------------------------+-----------------------+-----------------+--------------------+-----------+------------------+------------------+------------+------------------
|
||||
3 | 2 | | 1 | 3 | 2 | single | _timescaledb_internal | _hyper_2_2_0_3_data | 2 | 2 | 2 | 0 | _timescaledb_internal | _hyper_2_2_0_partition | 2 | public | chunk_closing_test | _timescaledb_internal | _hyper_2 | _timescaledb_internal | _hyper_2_root | 1 | STICKY | time | bigint | single | 10000
|
||||
4 | 2 | 2 | 2 | 4 | 2 | single | _timescaledb_internal | _hyper_2_2_0_4_data | 2 | 2 | 2 | 0 | _timescaledb_internal | _hyper_2_2_0_partition | 2 | public | chunk_closing_test | _timescaledb_internal | _hyper_2 | _timescaledb_internal | _hyper_2_root | 1 | STICKY | time | bigint | single | 10000
|
||||
5 | 2 | 3 | | 5 | 2 | single | _timescaledb_internal | _hyper_2_2_0_5_data | 2 | 2 | 2 | 0 | _timescaledb_internal | _hyper_2_2_0_partition | 2 | public | chunk_closing_test | _timescaledb_internal | _hyper_2 | _timescaledb_internal | _hyper_2_root | 1 | STICKY | time | bigint | single | 10000
|
||||
(3 rows)
|
||||
|
||||
\set VERBOSITY default
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE ROLE alt_usr LOGIN;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN
|
||||
--mute error
|
||||
END$$;
|
||||
\c single alt_usr
|
||||
\dt
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+--------------------+-------+----------
|
||||
public | chunk_closing_test | table | postgres
|
||||
public | testNs | table | postgres
|
||||
(2 rows)
|
||||
|
||||
\set ON_ERROR_STOP 0
|
||||
SELECT * FROM chunk_closing_test;
|
||||
ERROR: permission denied for relation _hyper_2_0_replica
|
||||
--todo fix error message here:
|
||||
SELECT * FROM "testNs";
|
||||
ERROR: permission denied for schema testNs
|
||||
\set ON_ERROR_STOP 1
|
||||
CREATE TABLE "1dim"(time timestamp, temp float);
|
||||
SELECT create_hypertable('"1dim"', 'time');
|
||||
create_hypertable
|
||||
-------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
INSERT INTO "1dim" VALUES('2017-01-20T09:00:01', 22.5);
|
||||
INSERT INTO "1dim" VALUES('2017-01-20T09:00:21', 21.2);
|
||||
INSERT INTO "1dim" VALUES('2017-01-20T09:00:47', 25.1);
|
||||
SELECT * FROM "1dim";
|
||||
time | temp
|
||||
--------------------------+------
|
||||
Fri Jan 20 09:00:01 2017 | 22.5
|
||||
Fri Jan 20 09:00:21 2017 | 21.2
|
||||
Fri Jan 20 09:00:47 2017 | 25.1
|
||||
(3 rows)
|
||||
|
||||
\ir include/ddl_ops_1.sql
|
||||
CREATE TABLE PUBLIC."Hypertable_1" (
|
||||
time BIGINT NOT NULL,
|
||||
"Device_id" TEXT NOT NULL,
|
||||
temp_c int NOT NULL DEFAULT -1,
|
||||
humidity numeric NULL DEFAULT 0,
|
||||
sensor_1 NUMERIC NULL DEFAULT 1,
|
||||
sensor_2 NUMERIC NOT NULL DEFAULT 1,
|
||||
sensor_3 NUMERIC NOT NULL DEFAULT 1,
|
||||
sensor_4 NUMERIC NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE INDEX ON PUBLIC."Hypertable_1" (time, "Device_id");
|
||||
SELECT * FROM create_hypertable('"public"."Hypertable_1"', 'time', 'Device_id', 1);
|
||||
create_hypertable
|
||||
-------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
SELECT * FROM _timescaledb_catalog.hypertable;
|
||||
id | schema_name | table_name | associated_schema_name | associated_table_prefix | root_schema_name | root_table_name | replication_factor | placement | time_column_name | time_column_type | created_on | chunk_size_bytes
|
||||
----+-------------+--------------------+------------------------+-------------------------+-----------------------+-----------------+--------------------+-----------+------------------+-----------------------------+------------+------------------
|
||||
1 | public | testNs | testNs | _hyper_1 | testNs | _hyper_1_root | 1 | STICKY | timeCustom | bigint | single | 1073741824
|
||||
2 | public | chunk_closing_test | _timescaledb_internal | _hyper_2 | _timescaledb_internal | _hyper_2_root | 1 | STICKY | time | bigint | single | 10000
|
||||
3 | public | 1dim | _timescaledb_internal | _hyper_3 | _timescaledb_internal | _hyper_3_root | 1 | STICKY | time | timestamp without time zone | single | 1073741824
|
||||
4 | public | Hypertable_1 | _timescaledb_internal | _hyper_4 | _timescaledb_internal | _hyper_4_root | 1 | STICKY | time | bigint | single | 1073741824
|
||||
(4 rows)
|
||||
|
||||
SELECT * FROM _timescaledb_catalog.hypertable_index;
|
||||
hypertable_id | main_schema_name | main_index_name | definition | created_on
|
||||
---------------+------------------+-----------------------------------+---------------------------------------------------------------------------------------------------------------------------------------+------------
|
||||
1 | public | testNs_device_id_timeCustom_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree (device_id, "timeCustom" DESC NULLS LAST) WHERE (device_id IS NOT NULL) | single
|
||||
1 | public | testNs_timeCustom_series_0_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("timeCustom" DESC NULLS LAST, series_0) WHERE (series_0 IS NOT NULL) | single
|
||||
1 | public | testNs_timeCustom_series_1_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("timeCustom" DESC NULLS LAST, series_1) WHERE (series_1 IS NOT NULL) | single
|
||||
1 | public | testNs_timeCustom_series_2_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("timeCustom" DESC NULLS LAST, series_2) WHERE (series_2 IS NOT NULL) | single
|
||||
1 | public | testNs_timeCustom_series_bool_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("timeCustom" DESC NULLS LAST, series_bool) WHERE (series_bool IS NOT NULL) | single
|
||||
4 | public | Hypertable_1_time_Device_id_idx | CREATE INDEX /*INDEX_NAME*/ ON /*TABLE_NAME*/ USING btree ("time", "Device_id") | single
|
||||
(6 rows)
|
||||
|
||||
CREATE INDEX ON PUBLIC."Hypertable_1" (time, "temp_c");
|
||||
CREATE INDEX "ind_humidity" ON PUBLIC."Hypertable_1" (time, "humidity");
|
||||
CREATE INDEX "ind_sensor_1" ON PUBLIC."Hypertable_1" (time, "sensor_1");
|
||||
INSERT INTO PUBLIC."Hypertable_1"(time, "Device_id", temp_c, humidity, sensor_1, sensor_2, sensor_3, sensor_4)
|
||||
VALUES(1257894000000000000, 'dev1', 30, 70, 1, 2, 3, 100);
|
||||
--expect error cases
|
||||
\set ON_ERROR_STOP 0
|
||||
UPDATE ONLY PUBLIC."Hypertable_1" SET time = 0 WHERE TRUE;
|
||||
psql:include/ddl_ops_1.sql:25: ERROR: UPDATE ONLY not supported on hypertables
|
||||
DELETE FROM ONLY PUBLIC."Hypertable_1" WHERE "Device_id" = 'dev1';
|
||||
psql:include/ddl_ops_1.sql:26: ERROR: DELETE ONLY not currently supported on hypertables
|
||||
\set ON_ERROR_STOP 1
|
||||
\ir include/ddl_ops_2.sql
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ADD COLUMN temp_f INTEGER NOT NULL DEFAULT 31;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" DROP COLUMN temp_c;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" DROP COLUMN sensor_4;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ALTER COLUMN humidity SET DEFAULT 100;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ALTER COLUMN sensor_1 DROP DEFAULT;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ALTER COLUMN sensor_2 SET DEFAULT NULL;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ALTER COLUMN sensor_1 SET NOT NULL;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ALTER COLUMN sensor_2 DROP NOT NULL;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" RENAME COLUMN sensor_2 TO sensor_2_renamed;
|
||||
ALTER TABLE PUBLIC."Hypertable_1" RENAME COLUMN sensor_3 TO sensor_3_renamed;
|
||||
DROP INDEX "ind_sensor_1";
|
||||
--expect error cases
|
||||
\set ON_ERROR_STOP 0
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ALTER COLUMN sensor_2_renamed SET DATA TYPE int;
|
||||
psql:include/ddl_ops_2.sql:15: ERROR: ALTER TABLE ... ALTER COLUMN SET DATA TYPE not supported on hypertable "Hypertable_1"
|
||||
ALTER INDEX "ind_humidity" RENAME TO "ind_humdity2";
|
||||
psql:include/ddl_ops_2.sql:16: ERROR: ALTER INDEX not supported on hypertable "Hypertable_1"
|
||||
\set ON_ERROR_STOP 1
|
||||
--create column with same name as previously renamed one
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ADD COLUMN sensor_3 BIGINT NOT NULL DEFAULT 131;
|
||||
--create column with same name as previously dropped one
|
||||
ALTER TABLE PUBLIC."Hypertable_1" ADD COLUMN sensor_4 BIGINT NOT NULL DEFAULT 131;
|
36
test/sql/alternate_users.sql
Normal file
36
test/sql/alternate_users.sql
Normal file
@ -0,0 +1,36 @@
|
||||
\set ON_ERROR_STOP 1
|
||||
\set VERBOSITY verbose
|
||||
\set SHOW_CONTEXT never
|
||||
|
||||
|
||||
\set ECHO ALL
|
||||
\ir include/insert_single.sql
|
||||
|
||||
\set VERBOSITY default
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE ROLE alt_usr LOGIN;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN
|
||||
--mute error
|
||||
END$$;
|
||||
|
||||
\c single alt_usr
|
||||
\dt
|
||||
|
||||
\set ON_ERROR_STOP 0
|
||||
SELECT * FROM chunk_closing_test;
|
||||
--todo fix error message here:
|
||||
SELECT * FROM "testNs";
|
||||
\set ON_ERROR_STOP 1
|
||||
|
||||
CREATE TABLE "1dim"(time timestamp, temp float);
|
||||
SELECT create_hypertable('"1dim"', 'time');
|
||||
INSERT INTO "1dim" VALUES('2017-01-20T09:00:01', 22.5);
|
||||
INSERT INTO "1dim" VALUES('2017-01-20T09:00:21', 21.2);
|
||||
INSERT INTO "1dim" VALUES('2017-01-20T09:00:47', 25.1);
|
||||
SELECT * FROM "1dim";
|
||||
|
||||
|
||||
\ir include/ddl_ops_1.sql
|
||||
\ir include/ddl_ops_2.sql
|
Loading…
x
Reference in New Issue
Block a user