diff options
Diffstat (limited to 'contrib/adminpack')
-rw-r--r-- | contrib/adminpack/.gitignore | 4 | ||||
-rw-r--r-- | contrib/adminpack/Makefile | 24 | ||||
-rw-r--r-- | contrib/adminpack/adminpack--1.0--1.1.sql | 6 | ||||
-rw-r--r-- | contrib/adminpack/adminpack--1.0.sql | 53 | ||||
-rw-r--r-- | contrib/adminpack/adminpack--1.1--2.0.sql | 51 | ||||
-rw-r--r-- | contrib/adminpack/adminpack--2.0--2.1.sql | 17 | ||||
-rw-r--r-- | contrib/adminpack/adminpack.c | 591 | ||||
-rw-r--r-- | contrib/adminpack/adminpack.control | 6 | ||||
-rw-r--r-- | contrib/adminpack/expected/adminpack.out | 172 | ||||
-rw-r--r-- | contrib/adminpack/meson.build | 35 | ||||
-rw-r--r-- | contrib/adminpack/sql/adminpack.sql | 76 |
11 files changed, 1035 insertions, 0 deletions
diff --git a/contrib/adminpack/.gitignore b/contrib/adminpack/.gitignore new file mode 100644 index 0000000..5dcb3ff --- /dev/null +++ b/contrib/adminpack/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/contrib/adminpack/Makefile b/contrib/adminpack/Makefile new file mode 100644 index 0000000..851504f --- /dev/null +++ b/contrib/adminpack/Makefile @@ -0,0 +1,24 @@ +# contrib/adminpack/Makefile + +MODULE_big = adminpack +OBJS = \ + $(WIN32RES) \ + adminpack.o + +EXTENSION = adminpack +DATA = adminpack--1.0.sql adminpack--1.0--1.1.sql adminpack--1.1--2.0.sql\ + adminpack--2.0--2.1.sql +PGFILEDESC = "adminpack - support functions for pgAdmin" + +REGRESS = adminpack + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/adminpack +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/adminpack/adminpack--1.0--1.1.sql b/contrib/adminpack/adminpack--1.0--1.1.sql new file mode 100644 index 0000000..bb58165 --- /dev/null +++ b/contrib/adminpack/adminpack--1.0--1.1.sql @@ -0,0 +1,6 @@ +/* contrib/adminpack/adminpack--1.0--1.1.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION adminpack UPDATE TO '1.1'" to load this file. \quit + +REVOKE EXECUTE ON FUNCTION pg_catalog.pg_logfile_rotate() FROM PUBLIC; diff --git a/contrib/adminpack/adminpack--1.0.sql b/contrib/adminpack/adminpack--1.0.sql new file mode 100644 index 0000000..f76f5c3 --- /dev/null +++ b/contrib/adminpack/adminpack--1.0.sql @@ -0,0 +1,53 @@ +/* contrib/adminpack/adminpack--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION adminpack" to load this file. \quit + +/* *********************************************** + * Administrative functions for PostgreSQL + * *********************************************** */ + +/* generic file access functions */ + +CREATE FUNCTION pg_catalog.pg_file_write(text, text, bool) +RETURNS bigint +AS 'MODULE_PATHNAME', 'pg_file_write' +LANGUAGE C VOLATILE STRICT; + +CREATE FUNCTION pg_catalog.pg_file_rename(text, text, text) +RETURNS bool +AS 'MODULE_PATHNAME', 'pg_file_rename' +LANGUAGE C VOLATILE; + +CREATE FUNCTION pg_catalog.pg_file_rename(text, text) +RETURNS bool +AS 'SELECT pg_catalog.pg_file_rename($1, $2, NULL::pg_catalog.text);' +LANGUAGE SQL VOLATILE STRICT; + +CREATE FUNCTION pg_catalog.pg_file_unlink(text) +RETURNS bool +AS 'MODULE_PATHNAME', 'pg_file_unlink' +LANGUAGE C VOLATILE STRICT; + +CREATE FUNCTION pg_catalog.pg_logdir_ls() +RETURNS setof record +AS 'MODULE_PATHNAME', 'pg_logdir_ls' +LANGUAGE C VOLATILE STRICT; + + +/* Renaming of existing backend functions for pgAdmin compatibility */ + +CREATE FUNCTION pg_catalog.pg_file_read(text, bigint, bigint) +RETURNS text +AS 'pg_read_file' +LANGUAGE INTERNAL VOLATILE STRICT; + +CREATE FUNCTION pg_catalog.pg_file_length(text) +RETURNS bigint +AS 'SELECT size FROM pg_catalog.pg_stat_file($1)' +LANGUAGE SQL VOLATILE STRICT; + +CREATE FUNCTION pg_catalog.pg_logfile_rotate() +RETURNS int4 +AS 'pg_rotate_logfile' +LANGUAGE INTERNAL VOLATILE STRICT; diff --git a/contrib/adminpack/adminpack--1.1--2.0.sql b/contrib/adminpack/adminpack--1.1--2.0.sql new file mode 100644 index 0000000..ceaeafa --- /dev/null +++ b/contrib/adminpack/adminpack--1.1--2.0.sql @@ -0,0 +1,51 @@ +/* contrib/adminpack/adminpack--1.1--2.0.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION adminpack UPDATE TO '2.0'" to load this file. \quit + +/* *********************************************** + * Administrative functions for PostgreSQL + * *********************************************** */ + +/* generic file access functions */ + +CREATE OR REPLACE FUNCTION pg_catalog.pg_file_write(text, text, bool) +RETURNS bigint +AS 'MODULE_PATHNAME', 'pg_file_write_v1_1' +LANGUAGE C VOLATILE STRICT; + +REVOKE EXECUTE ON FUNCTION pg_catalog.pg_file_write(text, text, bool) FROM PUBLIC; + +CREATE OR REPLACE FUNCTION pg_catalog.pg_file_rename(text, text, text) +RETURNS bool +AS 'MODULE_PATHNAME', 'pg_file_rename_v1_1' +LANGUAGE C VOLATILE; + +REVOKE EXECUTE ON FUNCTION pg_catalog.pg_file_rename(text, text, text) FROM PUBLIC; + +CREATE OR REPLACE FUNCTION pg_catalog.pg_file_rename(text, text) +RETURNS bool +AS 'SELECT pg_catalog.pg_file_rename($1, $2, NULL::pg_catalog.text);' +LANGUAGE SQL VOLATILE STRICT; + +CREATE OR REPLACE FUNCTION pg_catalog.pg_file_unlink(text) +RETURNS bool +AS 'MODULE_PATHNAME', 'pg_file_unlink_v1_1' +LANGUAGE C VOLATILE STRICT; + +REVOKE EXECUTE ON FUNCTION pg_catalog.pg_file_unlink(text) FROM PUBLIC; + +CREATE OR REPLACE FUNCTION pg_catalog.pg_logdir_ls() +RETURNS setof record +AS 'MODULE_PATHNAME', 'pg_logdir_ls_v1_1' +LANGUAGE C VOLATILE STRICT; + +REVOKE EXECUTE ON FUNCTION pg_catalog.pg_logdir_ls() FROM PUBLIC; + +/* These functions are now in the backend and callers should update to use those */ + +DROP FUNCTION pg_file_read(text, bigint, bigint); + +DROP FUNCTION pg_file_length(text); + +DROP FUNCTION pg_logfile_rotate(); diff --git a/contrib/adminpack/adminpack--2.0--2.1.sql b/contrib/adminpack/adminpack--2.0--2.1.sql new file mode 100644 index 0000000..1c6712e --- /dev/null +++ b/contrib/adminpack/adminpack--2.0--2.1.sql @@ -0,0 +1,17 @@ +/* contrib/adminpack/adminpack--2.0--2.1.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION adminpack UPDATE TO '2.1'" to load this file. \quit + +/* *********************************************** + * Administrative functions for PostgreSQL + * *********************************************** */ + +/* generic file access functions */ + +CREATE OR REPLACE FUNCTION pg_catalog.pg_file_sync(text) +RETURNS void +AS 'MODULE_PATHNAME', 'pg_file_sync' +LANGUAGE C VOLATILE STRICT; + +REVOKE EXECUTE ON FUNCTION pg_catalog.pg_file_sync(text) FROM PUBLIC; diff --git a/contrib/adminpack/adminpack.c b/contrib/adminpack/adminpack.c new file mode 100644 index 0000000..d3aec7b --- /dev/null +++ b/contrib/adminpack/adminpack.c @@ -0,0 +1,591 @@ +/*------------------------------------------------------------------------- + * + * adminpack.c + * + * + * Copyright (c) 2002-2023, PostgreSQL Global Development Group + * + * Author: Andreas Pflug <pgadmin@pse-consulting.de> + * + * IDENTIFICATION + * contrib/adminpack/adminpack.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include <sys/file.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "catalog/pg_authid.h" +#include "catalog/pg_type.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "postmaster/syslogger.h" +#include "storage/fd.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/datetime.h" + + +#ifdef WIN32 + +#ifdef rename +#undef rename +#endif + +#ifdef unlink +#undef unlink +#endif +#endif + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(pg_file_write); +PG_FUNCTION_INFO_V1(pg_file_write_v1_1); +PG_FUNCTION_INFO_V1(pg_file_sync); +PG_FUNCTION_INFO_V1(pg_file_rename); +PG_FUNCTION_INFO_V1(pg_file_rename_v1_1); +PG_FUNCTION_INFO_V1(pg_file_unlink); +PG_FUNCTION_INFO_V1(pg_file_unlink_v1_1); +PG_FUNCTION_INFO_V1(pg_logdir_ls); +PG_FUNCTION_INFO_V1(pg_logdir_ls_v1_1); + +static int64 pg_file_write_internal(text *file, text *data, bool replace); +static bool pg_file_rename_internal(text *file1, text *file2, text *file3); +static Datum pg_logdir_ls_internal(FunctionCallInfo fcinfo); + + +/*----------------------- + * some helper functions + */ + +/* + * Convert a "text" filename argument to C string, and check it's allowable. + * + * Filename may be absolute or relative to the DataDir, but we only allow + * absolute paths that match DataDir. + */ +static char * +convert_and_check_filename(text *arg) +{ + char *filename = text_to_cstring(arg); + + canonicalize_path(filename); /* filename can change length here */ + + /* + * Members of the 'pg_write_server_files' role are allowed to access any + * files on the server as the PG user, so no need to do any further checks + * here. + */ + if (has_privs_of_role(GetUserId(), ROLE_PG_WRITE_SERVER_FILES)) + return filename; + + /* + * User isn't a member of the pg_write_server_files role, so check if it's + * allowable + */ + if (is_absolute_path(filename)) + { + /* Allow absolute paths if within DataDir */ + if (!path_is_prefix_of_path(DataDir, filename)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("absolute path not allowed"))); + } + else if (!path_is_relative_and_below_cwd(filename)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("path must be in or below the data directory"))); + + return filename; +} + + +/* + * check for superuser, bark if not. + */ +static void +requireSuperuser(void) +{ + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("only superuser may access generic file functions"))); +} + + + +/* ------------------------------------ + * pg_file_write - old version + * + * The superuser() check here must be kept as the library might be upgraded + * without the extension being upgraded, meaning that in pre-1.1 installations + * these functions could be called by any user. + */ +Datum +pg_file_write(PG_FUNCTION_ARGS) +{ + text *file = PG_GETARG_TEXT_PP(0); + text *data = PG_GETARG_TEXT_PP(1); + bool replace = PG_GETARG_BOOL(2); + int64 count = 0; + + requireSuperuser(); + + count = pg_file_write_internal(file, data, replace); + + PG_RETURN_INT64(count); +} + +/* ------------------------------------ + * pg_file_write_v1_1 - Version 1.1 + * + * As of adminpack version 1.1, we no longer need to check if the user + * is a superuser because we REVOKE EXECUTE on the function from PUBLIC. + * Users can then grant access to it based on their policies. + * + * Otherwise identical to pg_file_write (above). + */ +Datum +pg_file_write_v1_1(PG_FUNCTION_ARGS) +{ + text *file = PG_GETARG_TEXT_PP(0); + text *data = PG_GETARG_TEXT_PP(1); + bool replace = PG_GETARG_BOOL(2); + int64 count = 0; + + count = pg_file_write_internal(file, data, replace); + + PG_RETURN_INT64(count); +} + +/* ------------------------------------ + * pg_file_write_internal - Workhorse for pg_file_write functions. + * + * This handles the actual work for pg_file_write. + */ +static int64 +pg_file_write_internal(text *file, text *data, bool replace) +{ + FILE *f; + char *filename; + int64 count = 0; + + filename = convert_and_check_filename(file); + + if (!replace) + { + struct stat fst; + + if (stat(filename, &fst) >= 0) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_FILE), + errmsg("file \"%s\" exists", filename))); + + f = AllocateFile(filename, "wb"); + } + else + f = AllocateFile(filename, "ab"); + + if (!f) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\" for writing: %m", + filename))); + + count = fwrite(VARDATA_ANY(data), 1, VARSIZE_ANY_EXHDR(data), f); + if (count != VARSIZE_ANY_EXHDR(data) || FreeFile(f)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", filename))); + + return (count); +} + +/* ------------------------------------ + * pg_file_sync + * + * We REVOKE EXECUTE on the function from PUBLIC. + * Users can then grant access to it based on their policies. + */ +Datum +pg_file_sync(PG_FUNCTION_ARGS) +{ + char *filename; + struct stat fst; + + filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0)); + + if (stat(filename, &fst) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", filename))); + + fsync_fname_ext(filename, S_ISDIR(fst.st_mode), false, ERROR); + + PG_RETURN_VOID(); +} + +/* ------------------------------------ + * pg_file_rename - old version + * + * The superuser() check here must be kept as the library might be upgraded + * without the extension being upgraded, meaning that in pre-1.1 installations + * these functions could be called by any user. + */ +Datum +pg_file_rename(PG_FUNCTION_ARGS) +{ + text *file1; + text *file2; + text *file3; + bool result; + + requireSuperuser(); + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + file1 = PG_GETARG_TEXT_PP(0); + file2 = PG_GETARG_TEXT_PP(1); + + if (PG_ARGISNULL(2)) + file3 = NULL; + else + file3 = PG_GETARG_TEXT_PP(2); + + result = pg_file_rename_internal(file1, file2, file3); + + PG_RETURN_BOOL(result); +} + +/* ------------------------------------ + * pg_file_rename_v1_1 - Version 1.1 + * + * As of adminpack version 1.1, we no longer need to check if the user + * is a superuser because we REVOKE EXECUTE on the function from PUBLIC. + * Users can then grant access to it based on their policies. + * + * Otherwise identical to pg_file_write (above). + */ +Datum +pg_file_rename_v1_1(PG_FUNCTION_ARGS) +{ + text *file1; + text *file2; + text *file3; + bool result; + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + file1 = PG_GETARG_TEXT_PP(0); + file2 = PG_GETARG_TEXT_PP(1); + + if (PG_ARGISNULL(2)) + file3 = NULL; + else + file3 = PG_GETARG_TEXT_PP(2); + + result = pg_file_rename_internal(file1, file2, file3); + + PG_RETURN_BOOL(result); +} + +/* ------------------------------------ + * pg_file_rename_internal - Workhorse for pg_file_rename functions. + * + * This handles the actual work for pg_file_rename. + */ +static bool +pg_file_rename_internal(text *file1, text *file2, text *file3) +{ + char *fn1, + *fn2, + *fn3; + int rc; + + fn1 = convert_and_check_filename(file1); + fn2 = convert_and_check_filename(file2); + + if (file3 == NULL) + fn3 = NULL; + else + fn3 = convert_and_check_filename(file3); + + if (access(fn1, W_OK) < 0) + { + ereport(WARNING, + (errcode_for_file_access(), + errmsg("file \"%s\" is not accessible: %m", fn1))); + + return false; + } + + if (fn3 && access(fn2, W_OK) < 0) + { + ereport(WARNING, + (errcode_for_file_access(), + errmsg("file \"%s\" is not accessible: %m", fn2))); + + return false; + } + + rc = access(fn3 ? fn3 : fn2, W_OK); + if (rc >= 0 || errno != ENOENT) + { + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_FILE), + errmsg("cannot rename to target file \"%s\"", + fn3 ? fn3 : fn2))); + } + + if (fn3) + { + if (rename(fn2, fn3) != 0) + { + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not rename \"%s\" to \"%s\": %m", + fn2, fn3))); + } + if (rename(fn1, fn2) != 0) + { + ereport(WARNING, + (errcode_for_file_access(), + errmsg("could not rename \"%s\" to \"%s\": %m", + fn1, fn2))); + + if (rename(fn3, fn2) != 0) + { + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not rename \"%s\" back to \"%s\": %m", + fn3, fn2))); + } + else + { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_FILE), + errmsg("renaming \"%s\" to \"%s\" was reverted", + fn2, fn3))); + } + } + } + else if (rename(fn1, fn2) != 0) + { + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not rename \"%s\" to \"%s\": %m", fn1, fn2))); + } + + return true; +} + + +/* ------------------------------------ + * pg_file_unlink - old version + * + * The superuser() check here must be kept as the library might be upgraded + * without the extension being upgraded, meaning that in pre-1.1 installations + * these functions could be called by any user. + */ +Datum +pg_file_unlink(PG_FUNCTION_ARGS) +{ + char *filename; + + requireSuperuser(); + + filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0)); + + if (access(filename, W_OK) < 0) + { + if (errno == ENOENT) + PG_RETURN_BOOL(false); + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("file \"%s\" is not accessible: %m", filename))); + } + + if (unlink(filename) < 0) + { + ereport(WARNING, + (errcode_for_file_access(), + errmsg("could not unlink file \"%s\": %m", filename))); + + PG_RETURN_BOOL(false); + } + PG_RETURN_BOOL(true); +} + + +/* ------------------------------------ + * pg_file_unlink_v1_1 - Version 1.1 + * + * As of adminpack version 1.1, we no longer need to check if the user + * is a superuser because we REVOKE EXECUTE on the function from PUBLIC. + * Users can then grant access to it based on their policies. + * + * Otherwise identical to pg_file_unlink (above). + */ +Datum +pg_file_unlink_v1_1(PG_FUNCTION_ARGS) +{ + char *filename; + + filename = convert_and_check_filename(PG_GETARG_TEXT_PP(0)); + + if (access(filename, W_OK) < 0) + { + if (errno == ENOENT) + PG_RETURN_BOOL(false); + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("file \"%s\" is not accessible: %m", filename))); + } + + if (unlink(filename) < 0) + { + ereport(WARNING, + (errcode_for_file_access(), + errmsg("could not unlink file \"%s\": %m", filename))); + + PG_RETURN_BOOL(false); + } + PG_RETURN_BOOL(true); +} + +/* ------------------------------------ + * pg_logdir_ls - Old version + * + * The superuser() check here must be kept as the library might be upgraded + * without the extension being upgraded, meaning that in pre-1.1 installations + * these functions could be called by any user. + */ +Datum +pg_logdir_ls(PG_FUNCTION_ARGS) +{ + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("only superuser can list the log directory"))); + + return (pg_logdir_ls_internal(fcinfo)); +} + +/* ------------------------------------ + * pg_logdir_ls_v1_1 - Version 1.1 + * + * As of adminpack version 1.1, we no longer need to check if the user + * is a superuser because we REVOKE EXECUTE on the function from PUBLIC. + * Users can then grant access to it based on their policies. + * + * Otherwise identical to pg_logdir_ls (above). + */ +Datum +pg_logdir_ls_v1_1(PG_FUNCTION_ARGS) +{ + return (pg_logdir_ls_internal(fcinfo)); +} + +static Datum +pg_logdir_ls_internal(FunctionCallInfo fcinfo) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + bool randomAccess; + TupleDesc tupdesc; + Tuplestorestate *tupstore; + AttInMetadata *attinmeta; + DIR *dirdesc; + struct dirent *de; + MemoryContext oldcontext; + + if (strcmp(Log_filename, "postgresql-%Y-%m-%d_%H%M%S.log") != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("the log_filename parameter must equal 'postgresql-%%Y-%%m-%%d_%%H%%M%%S.log'"))); + + /* check to see if caller supports us returning a tuplestore */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("materialize mode required, but it is not allowed in this context"))); + + /* The tupdesc and tuplestore must be created in ecxt_per_query_memory */ + oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); + + tupdesc = CreateTemplateTupleDesc(2); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "starttime", + TIMESTAMPOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "filename", + TEXTOID, -1, 0); + + randomAccess = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0; + tupstore = tuplestore_begin_heap(randomAccess, false, work_mem); + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + + MemoryContextSwitchTo(oldcontext); + + attinmeta = TupleDescGetAttInMetadata(tupdesc); + + dirdesc = AllocateDir(Log_directory); + while ((de = ReadDir(dirdesc, Log_directory)) != NULL) + { + char *values[2]; + HeapTuple tuple; + char timestampbuf[32]; + char *field[MAXDATEFIELDS]; + char lowstr[MAXDATELEN + 1]; + int dtype; + int nf, + ftype[MAXDATEFIELDS]; + fsec_t fsec; + int tz = 0; + struct pg_tm date; + DateTimeErrorExtra extra; + + /* + * Default format: postgresql-YYYY-MM-DD_HHMMSS.log + */ + if (strlen(de->d_name) != 32 + || strncmp(de->d_name, "postgresql-", 11) != 0 + || de->d_name[21] != '_' + || strcmp(de->d_name + 28, ".log") != 0) + continue; + + /* extract timestamp portion of filename */ + strcpy(timestampbuf, de->d_name + 11); + timestampbuf[17] = '\0'; + + /* parse and decode expected timestamp to verify it's OK format */ + if (ParseDateTime(timestampbuf, lowstr, MAXDATELEN, field, ftype, MAXDATEFIELDS, &nf)) + continue; + + if (DecodeDateTime(field, ftype, nf, + &dtype, &date, &fsec, &tz, &extra)) + continue; + + /* Seems the timestamp is OK; prepare and return tuple */ + + values[0] = timestampbuf; + values[1] = psprintf("%s/%s", Log_directory, de->d_name); + + tuple = BuildTupleFromCStrings(attinmeta, values); + + tuplestore_puttuple(tupstore, tuple); + } + + FreeDir(dirdesc); + return (Datum) 0; +} diff --git a/contrib/adminpack/adminpack.control b/contrib/adminpack/adminpack.control new file mode 100644 index 0000000..ae35d22 --- /dev/null +++ b/contrib/adminpack/adminpack.control @@ -0,0 +1,6 @@ +# adminpack extension +comment = 'administrative functions for PostgreSQL' +default_version = '2.1' +module_pathname = '$libdir/adminpack' +relocatable = false +schema = pg_catalog diff --git a/contrib/adminpack/expected/adminpack.out b/contrib/adminpack/expected/adminpack.out new file mode 100644 index 0000000..f419a5e --- /dev/null +++ b/contrib/adminpack/expected/adminpack.out @@ -0,0 +1,172 @@ +CREATE EXTENSION adminpack; +-- create new file +SELECT pg_file_write('test_file1', 'test1', false); + pg_file_write +--------------- + 5 +(1 row) + +SELECT pg_read_file('test_file1'); + pg_read_file +-------------- + test1 +(1 row) + +-- append +SELECT pg_file_write('test_file1', 'test1', true); + pg_file_write +--------------- + 5 +(1 row) + +SELECT pg_read_file('test_file1'); + pg_read_file +-------------- + test1test1 +(1 row) + +-- error, already exists +SELECT pg_file_write('test_file1', 'test1', false); +ERROR: file "test_file1" exists +SELECT pg_read_file('test_file1'); + pg_read_file +-------------- + test1test1 +(1 row) + +-- disallowed file paths for non-superusers and users who are +-- not members of pg_write_server_files +CREATE ROLE regress_adminpack_user1; +GRANT pg_read_all_settings TO regress_adminpack_user1; +GRANT EXECUTE ON FUNCTION pg_file_write(text,text,bool) TO regress_adminpack_user1; +SET ROLE regress_adminpack_user1; +SELECT pg_file_write('../test_file0', 'test0', false); +ERROR: path must be in or below the data directory +SELECT pg_file_write('/tmp/test_file0', 'test0', false); +ERROR: absolute path not allowed +SELECT pg_file_write(current_setting('data_directory') || '/test_file4', 'test4', false); + pg_file_write +--------------- + 5 +(1 row) + +SELECT pg_file_write(current_setting('data_directory') || '/../test_file4', 'test4', false); +ERROR: absolute path not allowed +RESET ROLE; +REVOKE EXECUTE ON FUNCTION pg_file_write(text,text,bool) FROM regress_adminpack_user1; +REVOKE pg_read_all_settings FROM regress_adminpack_user1; +DROP ROLE regress_adminpack_user1; +-- sync +SELECT pg_file_sync('test_file1'); -- sync file + pg_file_sync +-------------- + +(1 row) + +SELECT pg_file_sync('pg_stat'); -- sync directory + pg_file_sync +-------------- + +(1 row) + +SELECT pg_file_sync('test_file2'); -- not there +ERROR: could not stat file "test_file2": No such file or directory +-- rename file +SELECT pg_file_rename('test_file1', 'test_file2'); + pg_file_rename +---------------- + t +(1 row) + +SELECT pg_read_file('test_file1'); -- not there +ERROR: could not open file "test_file1" for reading: No such file or directory +SELECT pg_read_file('test_file2'); + pg_read_file +-------------- + test1test1 +(1 row) + +-- error +SELECT pg_file_rename('test_file1', 'test_file2'); +WARNING: file "test_file1" is not accessible: No such file or directory + pg_file_rename +---------------- + f +(1 row) + +-- rename file and archive +SELECT pg_file_write('test_file3', 'test3', false); + pg_file_write +--------------- + 5 +(1 row) + +SELECT pg_file_rename('test_file2', 'test_file3', 'test_file3_archive'); + pg_file_rename +---------------- + t +(1 row) + +SELECT pg_read_file('test_file2'); -- not there +ERROR: could not open file "test_file2" for reading: No such file or directory +SELECT pg_read_file('test_file3'); + pg_read_file +-------------- + test1test1 +(1 row) + +SELECT pg_read_file('test_file3_archive'); + pg_read_file +-------------- + test3 +(1 row) + +-- unlink +SELECT pg_file_unlink('test_file1'); -- does not exist + pg_file_unlink +---------------- + f +(1 row) + +SELECT pg_file_unlink('test_file2'); -- does not exist + pg_file_unlink +---------------- + f +(1 row) + +SELECT pg_file_unlink('test_file3'); + pg_file_unlink +---------------- + t +(1 row) + +SELECT pg_file_unlink('test_file3_archive'); + pg_file_unlink +---------------- + t +(1 row) + +SELECT pg_file_unlink('test_file4'); + pg_file_unlink +---------------- + t +(1 row) + +-- superuser checks +CREATE USER regress_adminpack_user1; +SET ROLE regress_adminpack_user1; +SELECT pg_file_write('test_file0', 'test0', false); +ERROR: permission denied for function pg_file_write +SELECT pg_file_sync('test_file0'); +ERROR: permission denied for function pg_file_sync +SELECT pg_file_rename('test_file0', 'test_file0'); +ERROR: permission denied for function pg_file_rename +CONTEXT: SQL function "pg_file_rename" statement 1 +SELECT pg_file_unlink('test_file0'); +ERROR: permission denied for function pg_file_unlink +SELECT pg_logdir_ls(); +ERROR: permission denied for function pg_logdir_ls +RESET ROLE; +DROP USER regress_adminpack_user1; +-- no further tests for pg_logdir_ls() because it depends on the +-- server's logging setup diff --git a/contrib/adminpack/meson.build b/contrib/adminpack/meson.build new file mode 100644 index 0000000..7658732 --- /dev/null +++ b/contrib/adminpack/meson.build @@ -0,0 +1,35 @@ +# Copyright (c) 2022-2023, PostgreSQL Global Development Group + +adminpack_sources = files( + 'adminpack.c', +) + +if host_system == 'windows' + adminpack_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'adminpack', + '--FILEDESC', 'adminpack - support functions for pgAdmin',]) +endif + +adminpack = shared_module('adminpack', + adminpack_sources, + kwargs: contrib_mod_args, +) +contrib_targets += adminpack + +install_data( + 'adminpack.control', + 'adminpack--1.0.sql', + 'adminpack--1.0--1.1.sql', + 'adminpack--1.1--2.0.sql', + 'adminpack--2.0--2.1.sql', + kwargs: contrib_data_args, +) + +tests += { + 'name': 'adminpack', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': ['adminpack'], + }, +} diff --git a/contrib/adminpack/sql/adminpack.sql b/contrib/adminpack/sql/adminpack.sql new file mode 100644 index 0000000..5776c9a --- /dev/null +++ b/contrib/adminpack/sql/adminpack.sql @@ -0,0 +1,76 @@ +CREATE EXTENSION adminpack; + +-- create new file +SELECT pg_file_write('test_file1', 'test1', false); +SELECT pg_read_file('test_file1'); + +-- append +SELECT pg_file_write('test_file1', 'test1', true); +SELECT pg_read_file('test_file1'); + +-- error, already exists +SELECT pg_file_write('test_file1', 'test1', false); +SELECT pg_read_file('test_file1'); + +-- disallowed file paths for non-superusers and users who are +-- not members of pg_write_server_files +CREATE ROLE regress_adminpack_user1; + +GRANT pg_read_all_settings TO regress_adminpack_user1; +GRANT EXECUTE ON FUNCTION pg_file_write(text,text,bool) TO regress_adminpack_user1; + +SET ROLE regress_adminpack_user1; +SELECT pg_file_write('../test_file0', 'test0', false); +SELECT pg_file_write('/tmp/test_file0', 'test0', false); +SELECT pg_file_write(current_setting('data_directory') || '/test_file4', 'test4', false); +SELECT pg_file_write(current_setting('data_directory') || '/../test_file4', 'test4', false); +RESET ROLE; +REVOKE EXECUTE ON FUNCTION pg_file_write(text,text,bool) FROM regress_adminpack_user1; +REVOKE pg_read_all_settings FROM regress_adminpack_user1; +DROP ROLE regress_adminpack_user1; + +-- sync +SELECT pg_file_sync('test_file1'); -- sync file +SELECT pg_file_sync('pg_stat'); -- sync directory +SELECT pg_file_sync('test_file2'); -- not there + +-- rename file +SELECT pg_file_rename('test_file1', 'test_file2'); +SELECT pg_read_file('test_file1'); -- not there +SELECT pg_read_file('test_file2'); + +-- error +SELECT pg_file_rename('test_file1', 'test_file2'); + +-- rename file and archive +SELECT pg_file_write('test_file3', 'test3', false); +SELECT pg_file_rename('test_file2', 'test_file3', 'test_file3_archive'); +SELECT pg_read_file('test_file2'); -- not there +SELECT pg_read_file('test_file3'); +SELECT pg_read_file('test_file3_archive'); + + +-- unlink +SELECT pg_file_unlink('test_file1'); -- does not exist +SELECT pg_file_unlink('test_file2'); -- does not exist +SELECT pg_file_unlink('test_file3'); +SELECT pg_file_unlink('test_file3_archive'); +SELECT pg_file_unlink('test_file4'); + + +-- superuser checks +CREATE USER regress_adminpack_user1; +SET ROLE regress_adminpack_user1; + +SELECT pg_file_write('test_file0', 'test0', false); +SELECT pg_file_sync('test_file0'); +SELECT pg_file_rename('test_file0', 'test_file0'); +SELECT pg_file_unlink('test_file0'); +SELECT pg_logdir_ls(); + +RESET ROLE; +DROP USER regress_adminpack_user1; + + +-- no further tests for pg_logdir_ls() because it depends on the +-- server's logging setup |