mirror of
https://github.com/timescale/timescaledb.git
synced 2025-05-16 02:23:49 +08:00
Previously, catalog tables were not fully protected from malicious non-superusers. This PR fixes permission handling be severely restricting permissions to the catalog and instead using SECURITY DEFINER functions to alter the catalog when needed without giving users permission to do those same operations outside of these functions. In addition, these functions check for proper permissions themselves so are safe to use. This PR also makes sure that chunk tables have the same owner as the hypertable and correctly handles `ALTER TABLE...OWNER TO` commands to keep this info in sync.
298 lines
10 KiB
PL/PgSQL
298 lines
10 KiB
PL/PgSQL
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_get_dimension_constraint_sql(
|
|
dimension_id INTEGER,
|
|
dimension_value BIGINT
|
|
)
|
|
RETURNS TEXT LANGUAGE SQL IMMUTABLE AS
|
|
$BODY$
|
|
SELECT format($$
|
|
SELECT cc.chunk_id
|
|
FROM _timescaledb_catalog.dimension_slice ds
|
|
INNER JOIN _timescaledb_catalog.chunk_constraint cc ON (ds.id = cc.dimension_slice_id)
|
|
WHERE ds.dimension_id = %1$L and ds.range_start <= %2$L and ds.range_end > %2$L
|
|
$$,
|
|
dimension_id, dimension_value);
|
|
$BODY$;
|
|
|
|
-- get a chunk if it exists
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_get_dimensions_constraint_sql(
|
|
dimension_ids INTEGER[],
|
|
dimension_values BIGINT[]
|
|
)
|
|
RETURNS TEXT LANGUAGE SQL STABLE AS
|
|
$BODY$
|
|
|
|
SELECT string_agg(_timescaledb_internal.chunk_get_dimension_constraint_sql(dimension_id,
|
|
dimension_value),
|
|
' INTERSECT ')
|
|
FROM (SELECT unnest(dimension_ids) AS dimension_id,
|
|
unnest(dimension_values) AS dimension_value
|
|
) AS sub;
|
|
$BODY$;
|
|
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_id_get_by_dimensions(
|
|
dimension_ids INTEGER[],
|
|
dimension_values BIGINT[]
|
|
)
|
|
RETURNS SETOF INTEGER LANGUAGE PLPGSQL STABLE AS
|
|
$BODY$
|
|
BEGIN
|
|
IF array_length(dimension_ids, 1) > 0 THEN
|
|
RETURN QUERY EXECUTE _timescaledb_internal.chunk_get_dimensions_constraint_sql(dimension_ids,
|
|
dimension_values);
|
|
END IF;
|
|
END
|
|
$BODY$;
|
|
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_get(
|
|
dimension_ids INTEGER[],
|
|
dimension_values BIGINT[]
|
|
)
|
|
RETURNS _timescaledb_catalog.chunk LANGUAGE PLPGSQL STABLE AS
|
|
$BODY$
|
|
DECLARE
|
|
chunk_row _timescaledb_catalog.chunk;
|
|
BEGIN
|
|
SELECT *
|
|
INTO chunk_row
|
|
FROM _timescaledb_catalog.chunk
|
|
WHERE
|
|
id = (SELECT _timescaledb_internal.chunk_id_get_by_dimensions(dimension_ids,
|
|
dimension_values));
|
|
RETURN chunk_row;
|
|
EXCEPTION
|
|
WHEN NO_DATA_FOUND THEN
|
|
RETURN NULL;
|
|
END
|
|
$BODY$;
|
|
|
|
--todo: unit test
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.dimension_calculate_default_range(
|
|
dimension_id INTEGER,
|
|
dimension_value BIGINT,
|
|
OUT range_start BIGINT,
|
|
OUT range_end BIGINT)
|
|
LANGUAGE PLPGSQL STABLE AS
|
|
$BODY$
|
|
DECLARE
|
|
dimension_row _timescaledb_catalog.dimension;
|
|
inter BIGINT;
|
|
BEGIN
|
|
SELECT *
|
|
FROM _timescaledb_catalog.dimension
|
|
INTO STRICT dimension_row
|
|
WHERE id = dimension_id;
|
|
|
|
IF dimension_row.interval_length IS NOT NULL THEN
|
|
range_start := (dimension_value / dimension_row.interval_length) * dimension_row.interval_length;
|
|
range_end := range_start + dimension_row.interval_length;
|
|
ELSE
|
|
inter := (2147483647 / dimension_row.num_slices);
|
|
IF dimension_value >= inter * (dimension_row.num_slices - 1) THEN
|
|
--put overflow from integer-division errors in last range
|
|
range_start = inter * (dimension_row.num_slices - 1);
|
|
range_end = 2147483647;
|
|
ELSE
|
|
range_start = (dimension_value / inter) * inter;
|
|
range_end := range_start + inter;
|
|
END IF;
|
|
END IF;
|
|
END
|
|
$BODY$;
|
|
|
|
-- calculate the range for a free dimension.
|
|
-- assumes one other fixed dimension.
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_calculate_new_ranges(
|
|
free_dimension_id INTEGER,
|
|
free_dimension_value BIGINT,
|
|
fixed_dimension_ids INTEGER[],
|
|
fixed_dimension_values BIGINT[],
|
|
align BOOLEAN,
|
|
OUT new_range_start BIGINT,
|
|
OUT new_range_end BIGINT
|
|
)
|
|
LANGUAGE PLPGSQL STABLE AS
|
|
$BODY$
|
|
DECLARE
|
|
overlap_value BIGINT;
|
|
alignment_found BOOLEAN := FALSE;
|
|
BEGIN
|
|
new_range_start := NULL;
|
|
new_range_end := NULL;
|
|
|
|
IF align THEN
|
|
--if i am aligning then fix see if other chunks have values that fit me in the free dimension
|
|
SELECT free_slice.range_start, free_slice.range_end
|
|
INTO new_range_start, new_range_end
|
|
FROM _timescaledb_catalog.chunk c
|
|
INNER JOIN _timescaledb_catalog.chunk_constraint cc ON (cc.chunk_id = c.id)
|
|
INNER JOIN _timescaledb_catalog.dimension_slice free_slice ON (free_slice.id = cc.dimension_slice_id AND free_slice.dimension_id = free_dimension_id)
|
|
WHERE
|
|
free_slice.range_end > free_dimension_value and free_slice.range_start <= free_dimension_value
|
|
LIMIT 1;
|
|
|
|
SELECT new_range_start IS NOT NULL INTO alignment_found;
|
|
END IF;
|
|
|
|
IF NOT alignment_found THEN
|
|
--either not aligned or did not find an alignment
|
|
SELECT *
|
|
INTO new_range_start, new_range_end
|
|
FROM _timescaledb_internal.dimension_calculate_default_range(free_dimension_id, free_dimension_value);
|
|
END IF;
|
|
|
|
-- Check whether the new chunk interval overlaps with existing chunks.
|
|
-- new_range_start overlaps
|
|
SELECT free_slice.range_end
|
|
INTO overlap_value
|
|
FROM _timescaledb_catalog.chunk c
|
|
INNER JOIN _timescaledb_catalog.chunk_constraint cc ON (cc.chunk_id = c.id)
|
|
INNER JOIN _timescaledb_catalog.dimension_slice free_slice ON (free_slice.id = cc.dimension_slice_id AND free_slice.dimension_id = free_dimension_id)
|
|
WHERE
|
|
c.id = (
|
|
SELECT _timescaledb_internal.chunk_id_get_by_dimensions(free_dimension_id || fixed_dimension_ids,
|
|
new_range_start || fixed_dimension_values)
|
|
)
|
|
ORDER BY free_slice.range_end DESC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
-- There is a chunk that overlaps with new_range_start, cut
|
|
-- new_range_start to begin where that chunk ends
|
|
IF alignment_found THEN
|
|
RAISE EXCEPTION 'Should never happen: needed to cut an aligned dimension'
|
|
USING ERRCODE = 'IO501';
|
|
END IF;
|
|
new_range_start := overlap_value;
|
|
END IF;
|
|
|
|
--check for new_range_end overlap
|
|
SELECT free_slice.range_start
|
|
INTO overlap_value
|
|
FROM _timescaledb_catalog.chunk c
|
|
INNER JOIN _timescaledb_catalog.chunk_constraint cc
|
|
ON (cc.chunk_id = c.id)
|
|
INNER JOIN _timescaledb_catalog.dimension_slice free_slice
|
|
ON (free_slice.id = cc.dimension_slice_id AND free_slice.dimension_id = free_dimension_id)
|
|
WHERE
|
|
c.id = (
|
|
SELECT _timescaledb_internal.chunk_id_get_by_dimensions(free_dimension_id || fixed_dimension_ids,
|
|
new_range_end || fixed_dimension_values)
|
|
)
|
|
ORDER BY free_slice.range_start ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
-- there is at least one table that starts inside, cut the end to match
|
|
IF alignment_found THEN
|
|
RAISE EXCEPTION 'Should never happen: needed to cut an aligned dimension'
|
|
USING ERRCODE = 'IO501';
|
|
END IF;
|
|
new_range_end := overlap_value;
|
|
END IF;
|
|
END
|
|
$BODY$;
|
|
|
|
-- creates the row in the chunk table. Prerequisite: appropriate lock.
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_create_after_lock(
|
|
dimension_ids INTEGER[],
|
|
dimension_values BIGINT[]
|
|
)
|
|
RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
|
|
$BODY$
|
|
DECLARE
|
|
dimension_row _timescaledb_catalog.dimension;
|
|
hypertable_id INTEGER;
|
|
free_index INTEGER;
|
|
fixed_dimension_ids INTEGER[];
|
|
fixed_values BIGINT[];
|
|
free_range_start BIGINT;
|
|
free_range_end BIGINT;
|
|
slice_ids INTEGER[];
|
|
slice_id INTEGER;
|
|
BEGIN
|
|
SELECT d.hypertable_id
|
|
INTO STRICT hypertable_id
|
|
FROM _timescaledb_catalog.dimension d
|
|
WHERE d.id = dimension_ids[1];
|
|
|
|
slice_ids = NULL;
|
|
FOR free_index IN 1 .. array_upper(dimension_ids, 1)
|
|
LOOP
|
|
--keep one dimension free and the rest fixed
|
|
fixed_dimension_ids = dimension_ids[:free_index-1]
|
|
|| dimension_ids[free_index+1:];
|
|
fixed_values = dimension_values[:free_index-1]
|
|
|| dimension_values[free_index+1:];
|
|
|
|
SELECT *
|
|
INTO STRICT dimension_row
|
|
FROM _timescaledb_catalog.dimension
|
|
WHERE id = dimension_ids[free_index];
|
|
|
|
SELECT *
|
|
INTO free_range_start, free_range_end
|
|
FROM _timescaledb_internal.chunk_calculate_new_ranges(
|
|
dimension_ids[free_index], dimension_values[free_index],
|
|
fixed_dimension_ids, fixed_values, dimension_row.aligned);
|
|
|
|
--do not use RETURNING here (ON CONFLICT DO NOTHING)
|
|
INSERT INTO _timescaledb_catalog.dimension_slice
|
|
(dimension_id, range_start, range_end)
|
|
VALUES(dimension_ids[free_index], free_range_start, free_range_end)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
SELECT id INTO STRICT slice_id
|
|
FROM _timescaledb_catalog.dimension_slice ds
|
|
WHERE ds.dimension_id = dimension_ids[free_index] AND
|
|
ds.range_start = free_range_start AND ds.range_end = free_range_end;
|
|
|
|
slice_ids = slice_ids || slice_id;
|
|
END LOOP;
|
|
|
|
WITH chunk AS (
|
|
INSERT INTO _timescaledb_catalog.chunk (id, hypertable_id, schema_name, table_name)
|
|
SELECT seq_id, h.id, h.associated_schema_name,
|
|
format('%s_%s_chunk', h.associated_table_prefix, seq_id)
|
|
FROM
|
|
nextval(pg_get_serial_sequence('_timescaledb_catalog.chunk','id')) seq_id,
|
|
_timescaledb_catalog.hypertable h
|
|
WHERE h.id = hypertable_id
|
|
RETURNING *
|
|
)
|
|
INSERT INTO _timescaledb_catalog.chunk_constraint (dimension_slice_id, chunk_id)
|
|
SELECT slice_id_to_insert, chunk.id FROM chunk, unnest(slice_ids) AS slice_id_to_insert;
|
|
END
|
|
$BODY$;
|
|
|
|
-- Creates and returns a new chunk, taking a lock on the chunk table.
|
|
-- static
|
|
CREATE OR REPLACE FUNCTION _timescaledb_internal.chunk_create(
|
|
dimension_ids INTEGER[],
|
|
dimension_values BIGINT[]
|
|
)
|
|
RETURNS _timescaledb_catalog.chunk LANGUAGE PLPGSQL VOLATILE
|
|
SECURITY DEFINER SET search_path = ''
|
|
AS
|
|
$BODY$
|
|
DECLARE
|
|
chunk_row _timescaledb_catalog.chunk;
|
|
BEGIN
|
|
LOCK TABLE _timescaledb_catalog.chunk IN EXCLUSIVE MODE;
|
|
|
|
-- recheck:
|
|
chunk_row := _timescaledb_internal.chunk_get(dimension_ids, dimension_values);
|
|
|
|
IF chunk_row IS NULL THEN
|
|
PERFORM _timescaledb_internal.chunk_create_after_lock(dimension_ids, dimension_values);
|
|
chunk_row := _timescaledb_internal.chunk_get(dimension_ids, dimension_values);
|
|
END IF;
|
|
|
|
IF chunk_row IS NULL THEN -- recheck
|
|
RAISE EXCEPTION 'Should never happen: chunk not found after creation'
|
|
USING ERRCODE = 'IO501';
|
|
END IF;
|
|
|
|
RETURN chunk_row;
|
|
END
|
|
$BODY$;
|