1
0
mirror of https://github.com/timescale/timescaledb.git synced 2025-05-14 17:43:34 +08:00
Erik Nordström 4df8f287a6 Add proper permissions handling for associated (chunk) schemas
A hypertable's associated schema is used to create and store internal
data tables (chunks). A hypertable creates tables in that schema,
typically with full superuser permissions, regardless of whether the
hypertable's owner or the current user have permissions for the schema.
If the schema doesn't exist, the hypertable will create it when
creating the first chunk, even though the user or table owner does
not have permissions to create schemas in the database.

This change adds proper permissions checks to create_hypertable() so
that users cannot create hypertables with a custom associated schema
unless they have the proper permissions on the schema or the database.

Chunks are also no longer created with internal schema permissions if
the associated schema is something different from the internal schema.
2017-12-28 11:24:29 +01:00

331 lines
12 KiB
PL/PgSQL

-- This file defines DDL functions for adding and manipulating hypertables.
-- Converts a regular postgres table to a hypertable.
--
-- main_table - The OID of the table to be converted
-- time_column_name - Name of the column that contains time for a given record
-- partitioning_column - Name of the column to partition data by
-- number_partitions - (Optional) Number of partitions for data
-- associated_schema_name - (Optional) Schema for internal hypertable tables
-- associated_table_prefix - (Optional) Prefix for internal hypertable table names
-- chunk_time_interval - (Optional) Initial time interval for a chunk
-- create_default_indexes - (Optional) Whether or not to create the default indexes
-- if_not_exists - (Optional) Do not fail if table is already a hypertable
-- partitioning_func - (Optional) The partitioning function to use for spatial partitioning
CREATE OR REPLACE FUNCTION create_hypertable(
main_table REGCLASS,
time_column_name NAME,
partitioning_column NAME = NULL,
number_partitions INTEGER = NULL,
associated_schema_name NAME = NULL,
associated_table_prefix NAME = NULL,
chunk_time_interval anyelement = NULL::bigint,
create_default_indexes BOOLEAN = TRUE,
if_not_exists BOOLEAN = FALSE,
partitioning_func REGPROC = NULL
)
RETURNS VOID LANGUAGE PLPGSQL VOLATILE
SET search_path = '_timescaledb_catalog'
AS
$BODY$
<<vars>>
DECLARE
hypertable_row _timescaledb_catalog.hypertable;
table_name NAME;
schema_name NAME;
tablespace_oid OID;
tablespace_name NAME;
main_table_has_items BOOLEAN;
is_hypertable BOOLEAN;
is_partitioned BOOLEAN;
chunk_time_interval_actual BIGINT;
time_type REGTYPE;
relowner OID;
BEGIN
-- Early abort if lacking permissions
PERFORM _timescaledb_internal.check_role(main_table);
SELECT c.relname, n.nspname, c.reltablespace, c.relkind = 'p', c.relowner
INTO STRICT table_name, schema_name, tablespace_oid, is_partitioned, relowner
FROM pg_class c
INNER JOIN pg_namespace n ON (n.OID = c.relnamespace)
WHERE c.OID = main_table;
-- Check that the user has permissions to create chunks in the
-- associated schema
PERFORM _timescaledb_internal.check_associated_schema_permissions(associated_schema_name, relowner);
IF is_partitioned THEN
RAISE EXCEPTION 'table % is already partitioned', main_table
USING ERRCODE = 'IO110';
END IF;
-- tables that don't have an associated tablespace has reltablespace OID set to 0
-- in pg_class and there is no matching row in pg_tablespace
SELECT spcname
INTO tablespace_name
FROM pg_tablespace t
WHERE t.OID = tablespace_oid;
EXECUTE format('SELECT TRUE FROM _timescaledb_catalog.hypertable WHERE
hypertable.schema_name = %L AND
hypertable.table_name = %L',
schema_name, table_name) INTO is_hypertable;
IF is_hypertable THEN
IF if_not_exists THEN
RAISE NOTICE 'hypertable % already exists, skipping', main_table;
RETURN;
ELSE
RAISE EXCEPTION 'hypertable % already exists', main_table
USING ERRCODE = 'IO110';
END IF;
END IF;
EXECUTE format('SELECT TRUE FROM %s LIMIT 1', main_table) INTO main_table_has_items;
PERFORM _timescaledb_internal.set_time_column_constraint(main_table, time_column_name);
IF main_table_has_items THEN
RAISE EXCEPTION 'the table being converted to a hypertable must be empty'
USING ERRCODE = 'IO102';
END IF;
-- Validate that the hypertable supports the triggers in the main table
PERFORM _timescaledb_internal.validate_triggers(main_table);
time_type := _timescaledb_internal.dimension_type(main_table, time_column_name, true);
chunk_time_interval_actual := _timescaledb_internal.time_interval_specification_to_internal_with_default_time(
time_type, chunk_time_interval, 'chunk_time_interval');
BEGIN
SELECT *
INTO hypertable_row
FROM _timescaledb_internal.create_hypertable(
main_table,
schema_name,
table_name,
time_column_name,
partitioning_column,
number_partitions,
associated_schema_name,
associated_table_prefix,
chunk_time_interval_actual,
tablespace_name,
create_default_indexes,
partitioning_func
);
EXCEPTION
WHEN unique_violation THEN
IF if_not_exists THEN
RAISE NOTICE 'hypertable % already exists, skipping', main_table;
RETURN;
ELSE
RAISE EXCEPTION 'hypertable % already exists', main_table
USING ERRCODE = 'IO110';
END IF;
WHEN foreign_key_violation THEN
RAISE EXCEPTION 'database not configured for hypertable storage (not setup as a data-node)'
USING ERRCODE = 'IO101';
END;
END
$BODY$;
-- Add a dimension (of partitioning) to a hypertable
--
-- main_table - OID of the table to add a dimension to
-- column_name - NAME of the column to use in partitioning for this dimension
-- number_partitions - Number of partitions, for non-time dimensions
-- interval_length - Size of intervals for time dimensions (can be integral or INTERVAL)
-- partitioning_func - Function used to partition the column
CREATE OR REPLACE FUNCTION add_dimension(
main_table REGCLASS,
column_name NAME,
number_partitions INTEGER = NULL,
interval_length anyelement = NULL::BIGINT,
partitioning_func REGPROC = NULL
)
RETURNS VOID LANGUAGE PLPGSQL VOLATILE
SECURITY DEFINER SET search_path = '_timescaledb_catalog'
AS
$BODY$
<<main_block>>
DECLARE
table_name NAME;
schema_name NAME;
hypertable_row _timescaledb_catalog.hypertable;
BEGIN
PERFORM _timescaledb_internal.check_role(main_table);
SELECT relname, nspname
INTO STRICT table_name, schema_name
FROM pg_class c
INNER JOIN pg_namespace n ON (n.OID = c.relnamespace)
WHERE c.OID = main_table;
SELECT *
INTO STRICT hypertable_row
FROM _timescaledb_catalog.hypertable h
WHERE h.schema_name = main_block.schema_name
AND h.table_name = main_block.table_name
FOR UPDATE;
PERFORM _timescaledb_internal.add_dimension(main_table,
hypertable_row,
column_name,
number_partitions,
interval_length,
partitioning_func);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE EXCEPTION '"%" is not a hypertable; cannot add dimension', main_block.table_name
USING ERRCODE = 'IO102';
END
$BODY$;
-- Update chunk_time_interval for a hypertable.
--
-- main_table - The OID of the table corresponding to a hypertable whose time
-- interval should be updated
-- chunk_time_interval - The new time interval. For hypertables with integral
-- time columns, this must be an integral type. For hypertables with a
-- TIMESTAMP/TIMESTAMPTZ/DATE type, it can be integral which is treated as
-- microseconds, or an INTERVAL type.
CREATE OR REPLACE FUNCTION set_chunk_time_interval(
main_table REGCLASS,
chunk_time_interval anyelement
)
RETURNS VOID LANGUAGE PLPGSQL VOLATILE
SECURITY DEFINER SET search_path='_timescaledb_catalog'
AS
$BODY$
DECLARE
main_table_name NAME;
main_schema_name NAME;
chunk_time_interval_actual BIGINT;
time_dimension _timescaledb_catalog.dimension;
time_type REGTYPE;
BEGIN
PERFORM _timescaledb_internal.check_role(main_table);
IF chunk_time_interval IS NULL THEN
RAISE EXCEPTION 'chunk_time_interval cannot be NULL'
USING ERRCODE = 'IO102';
END IF;
SELECT relname, nspname
INTO STRICT main_table_name, main_schema_name
FROM pg_class c
INNER JOIN pg_namespace n ON (n.OID = c.relnamespace)
WHERE c.OID = main_table;
SELECT *
INTO STRICT time_dimension
FROM _timescaledb_internal.dimension_get_time(
(
SELECT id
FROM _timescaledb_catalog.hypertable h
WHERE h.schema_name = main_schema_name AND
h.table_name = main_table_name
));
time_type := _timescaledb_internal.dimension_type(main_table, time_dimension.column_name, true);
chunk_time_interval_actual := _timescaledb_internal.time_interval_specification_to_internal_with_default_time(
time_type, chunk_time_interval, 'chunk_time_interval');
UPDATE _timescaledb_catalog.dimension d
SET interval_length = chunk_time_interval_actual
WHERE time_dimension.id = d.id;
END
$BODY$;
-- Drop chunks that are older than a timestamp.
CREATE OR REPLACE FUNCTION drop_chunks(
older_than anyelement,
table_name NAME = NULL,
schema_name NAME = NULL,
cascade BOOLEAN = FALSE
)
RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
older_than_internal BIGINT;
BEGIN
IF older_than IS NULL THEN
RAISE 'The timestamp provided to drop_chunks cannot be null';
END IF;
PERFORM _timescaledb_internal.drop_chunks_type_check(pg_typeof(older_than), table_name, schema_name);
SELECT _timescaledb_internal.time_to_internal(older_than, pg_typeof(older_than)) INTO older_than_internal;
PERFORM _timescaledb_internal.drop_chunks_impl(older_than_internal, table_name, schema_name, cascade);
END
$BODY$;
-- Drop chunks older than an interval.
CREATE OR REPLACE FUNCTION drop_chunks(
older_than INTERVAL,
table_name NAME = NULL,
schema_name NAME = NULL,
cascade BOOLEAN = false
)
RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
time_type REGTYPE;
BEGIN
BEGIN
WITH hypertable_ids AS (
SELECT id
FROM _timescaledb_catalog.hypertable h
WHERE (drop_chunks.schema_name IS NULL OR h.schema_name = drop_chunks.schema_name) AND
(drop_chunks.table_name IS NULL OR h.table_name = drop_chunks.table_name)
)
SELECT DISTINCT time_dim.column_type INTO STRICT time_type
FROM hypertable_ids INNER JOIN LATERAL _timescaledb_internal.dimension_get_time(hypertable_ids.id) time_dim ON (true);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE EXCEPTION 'No hypertables found';
WHEN TOO_MANY_ROWS THEN
RAISE EXCEPTION 'Cannot use drop_chunks on multiple tables with different time types';
END;
IF time_type = 'TIMESTAMP'::regtype THEN
PERFORM drop_chunks((now() - older_than)::timestamp, table_name, schema_name, cascade);
ELSIF time_type = 'DATE'::regtype THEN
PERFORM drop_chunks((now() - older_than)::date, table_name, schema_name, cascade);
ELSIF time_type = 'TIMESTAMPTZ'::regtype THEN
PERFORM drop_chunks(now() - older_than, table_name, schema_name, cascade);
ELSE
RAISE 'Can only use drop_chunks with an INTERVAL for TIMESTAMP, TIMESTAMPTZ, and DATE types';
END IF;
END
$BODY$;
CREATE OR REPLACE FUNCTION attach_tablespace(tablespace NAME, hypertable REGCLASS)
RETURNS VOID LANGUAGE SQL AS
$BODY$
SELECT * FROM _timescaledb_internal.attach_tablespace(tablespace, hypertable);
$BODY$;
-- Detach the given tablespace from a hypertable
CREATE OR REPLACE FUNCTION detach_tablespace(tablespace NAME, hypertable REGCLASS = NULL)
RETURNS INTEGER LANGUAGE SQL AS
$BODY$
SELECT * FROM _timescaledb_internal.detach_tablespace(tablespace, hypertable);
$BODY$;
-- Detach all tablespaces from the a hypertable
CREATE OR REPLACE FUNCTION detach_tablespaces(hypertable REGCLASS)
RETURNS INTEGER LANGUAGE SQL AS
$BODY$
SELECT * FROM _timescaledb_internal.detach_tablespaces(hypertable);
$BODY$;
CREATE OR REPLACE FUNCTION show_tablespaces(hypertable REGCLASS)
RETURNS SETOF NAME LANGUAGE SQL AS
$BODY$
SELECT * FROM _timescaledb_internal.show_tablespaces(hypertable);
$BODY$;