src/server/plugins/postgresql/config.c

Fri, 29 Nov 2024 18:10:26 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Fri, 29 Nov 2024 18:10:26 +0100
changeset 564
f19fd264c69c
parent 490
d218607f5a7e
permissions
-rw-r--r--

fix wrong column name usage by pg prop extensions

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2022 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#include "../../util/util.h"

#include <libxml/tree.h>

#define xstreq(a,b) xmlStrEqual(BAD_CAST a, BAD_CAST b)

static int pg_load_ext_dav_config(
        ServerConfiguration *cfg,
        pool_handle_t *pool,
        PgRepository *repo,
        const char *file);


static const char *sql_get_repository_root = "select resource_id from Resource where parent_id is NULL and nodename = $1 ;";


// Uses a PGconn to lookup the resource id of the specified root node
// if the lookup succeeds, the resource id is written to rootid
// in case of an error, an error message will be logged
// returns: 0 success, 1 error
int pg_lookup_root(ResourceData *res, const char *rootnode, int64_t *rootid) {
    PGconn *connection = res->data;
    
    PGresult *result = PQexecParams(
            connection,
            sql_get_repository_root,
            1,     // number of parameters
            NULL,
            &rootnode, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
    
    if(!result) {
        log_ereport(LOG_FAILURE, "pg: root lookup failed: %s", PQerrorMessage(connection));
        return 1;
    }
    
    int ret = 0;
    
    int nrows = PQntuples(result);
    if(nrows == 1) {
        char *resource_id_str = PQgetvalue(result, 0, 0);
        if(resource_id_str) {
            if(!util_strtoint(resource_id_str, rootid)) {
                log_ereport(LOG_FAILURE, "pg: unexpected result for column resource_id", rootnode);
                ret = 1;
            }
        }
    } else {
        log_ereport(LOG_FAILURE, "pg: cannot find root resource '%s'", rootnode);
        ret = 1;
    }
    PQclear(result);
    
    return ret;
}

PgRepository* pg_init_repo(ServerConfiguration *cfg, pool_handle_t *pool, WSConfigNode *config) {
    CxAllocator *a = pool_allocator(pool);
    
    ConfigNode *pg = serverconfig_get_node(config, CONFIG_NODE_OBJECT, cx_str("Postgresql"));
    if(!pg) {
        log_ereport(LOG_MISCONFIG, "pg_init_repo: missing postgresql config object");
        return NULL;
    }
    
    cxstring cfg_respool = serverconfig_object_directive_value(pg, cx_str("ResourcePool"));
    cxstring cfg_rootid = serverconfig_object_directive_value(pg, cx_str("RootId"));
    cxstring cfg_rootnode = serverconfig_object_directive_value(pg, cx_str("RootNode"));
    cxstring cfg_dav = serverconfig_object_directive_value(pg, cx_str("PGDavConfig"));
    
    // minimum requirement is a resource pool
    if(cfg_respool.length == 0) {
        return NULL;
    }
    
    // check rootid
    int64_t root_id = 1;
    if(cfg_rootid.length > 0) {
        if(!util_strtoint(cfg_rootid.ptr, &root_id)) {
            log_ereport(LOG_MISCONFIG, "pg_init_repo: RootId parameter is not an integer: %s", cfg_rootid.ptr);
            return NULL;
        }
    }
    
    // warn if RootId and RootNode are specified
    if(cfg_rootid.length > 0 && cfg_rootnode.length > 0) {
        log_ereport(LOG_WARN, "log_init_repo: RootId and RootNode specified, RootNode ignored");
    } else if(cfg_rootnode.length > 0) {
        // check root node
        
        // resolve rootnode
        // therefore we first need to get a connection from the resourcepool
        ResourceData *res = resourcepool_cfg_lookup(cfg, cfg_respool.ptr, 0);
        if(!res) {
            log_ereport(LOG_MISCONFIG, "pg_init_repo: resource lookup failed");
            return NULL;
        }
        // do lookup, if successful, root_id will be set
        int lookup_err = pg_lookup_root(res, cfg_rootnode.ptr, &root_id);
        // we don't need the connection anymore
        resourcepool_free(NULL, NULL, res);
        if(lookup_err) {
            // no logging required, pg_lookup_root will log the error
            return NULL;
        }
    }
    
    PgRepository *repo = pool_malloc(pool, sizeof(PgRepository));
    ZERO(repo, sizeof(PgRepository));
    
    repo->resourcepool = cx_strdup_a(a, cfg_respool);
    repo->root_resource_id = root_id;
    
    // check for extended pg dav config
    if(cfg_dav.length > 0) {
        // load extended config from config file
        char *cfg_file_path = cfg_config_file_path(cfg_dav.ptr);
        if(pg_load_ext_dav_config(cfg, pool, repo, cfg_file_path)) {
            // error
            repo = NULL; // no need to cleanup because everything is from the pool
        }
        free(cfg_file_path);
    }
    
    return repo;
}

static int pg_ext_get_config(
        ServerConfiguration *cfg,
        pool_handle_t *pool,
        PgRepository *repo,
        const char *file_path,
        xmlNode *root);
static int pg_ext_get_extension(
        PgExtParser *ext,
        pool_handle_t *pool,
        PgRepository *repo,
        const char *file_path,
        xmlNode *ext_node);

static const char* pg_util_xml_get_text(const xmlNode *elm) {
    xmlNode *node = elm->children;
    while(node) {
        if(node->type == XML_TEXT_NODE) {
            return (const char*)node->content;
        }
        node = node->next;
    }
    return NULL;
}

// load additional postgresql webdav config from the specified xml file
static int pg_load_ext_dav_config(
        ServerConfiguration *cfg,
        pool_handle_t *pool,
        PgRepository *repo,
        const char *file_path)
{
    CxAllocator *a = pool_allocator(pool);
    
    // check if the file exists and can be accessed
    struct stat s;
    if(stat(file_path, &s)) {
        if(errno == ENOENT) {
            log_ereport(LOG_FAILURE, "pg: config file %s not found", file_path);
        } else {
            log_ereport(LOG_FAILURE, "pg: cannot access config file %s", file_path);
        }
        
        return 1;
    }
    
    // prepare PgRepository
    repo->prop_ext = cxHashMapCreate(a, CX_STORE_POINTERS, 8);
    if(!repo->prop_ext) {
        log_ereport(LOG_FAILURE, "pg: cannot load config file: OOM");
        return 1;
    }
    
    // load xml document
    xmlDoc *doc = xmlReadFile(file_path, NULL, 0);
    if(!doc) {
        log_ereport(LOG_FAILURE, "pg: cannot load config file %s", file_path);
        return 1;
    }
    
    // the root element must be <repository>
    int ret = 0;
    xmlNode *xml_root = xmlDocGetRootElement(doc);
    if(xstreq(xml_root->name, "repository")) {
        // parse config
        ret = pg_ext_get_config(cfg, pool, repo, file_path, xml_root);
    } else {
        log_ereport(LOG_MISCONFIG, "pg: config %s: root element <repository> expected", file_path);
        ret = 1;
    }
    xmlFreeDoc(doc);
    
    return ret;
}




static int pg_ext_get_config(
        ServerConfiguration *cfg,
        pool_handle_t *pool,
        PgRepository *repo,
        const char *file_path,
        xmlNode *root)
{
    xmlNode *node = root->children;
    int ret = 0;
    
    PgExtParser parserData;
    parserData.table_lookup = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 8);
    parserData.tables = cxLinkedListCreate(cxDefaultAllocator, NULL, sizeof(PgExtTable));
    
    while(node && !ret) {
        // currently, the only possible config element is <extension>
        if(node->type == XML_ELEMENT_NODE) {
            if(xstreq(node->name, "extension")) {
                ret = pg_ext_get_extension(&parserData, pool, repo, file_path, node);
            }
        }
        node = node->next;
    }
    
    // convert parserData
    if(!ret) {
        size_t ntables = parserData.tables->size;
        repo->ntables = ntables;
        repo->tables = pool_calloc(pool, ntables, sizeof(PgExtTable));
        if(repo->tables) {
            int i = 0;
            CxIterator iter = cxListIterator(parserData.tables);
            cx_foreach(PgExtTable *, tab, iter) {
                repo->tables[i++] = *tab;
            }
        } else {
            ret = 1;
        }
        
    }
    
    // cleanup parser
    cxListDestroy(parserData.tables);
    cxMapDestroy(parserData.table_lookup);
    
    return ret;
}

static int pg_ext_get_extension(
        PgExtParser *ext,
        pool_handle_t *pool,
        PgRepository *repo,
        const char *file_path,
        xmlNode *ext_node)
{
    CxAllocator *a = pool_allocator(pool);
    
    xmlNode *node = ext_node->children;
    
    const char *table = NULL;
    CxList *properties = cxLinkedListCreate(a, NULL, CX_STORE_POINTERS);
    
    while(node) {
        if(node->type == XML_ELEMENT_NODE) {
            if(xstreq(node->name, "table")) {
                const char *value = pg_util_xml_get_text(node);
                if(!value) {
                    log_ereport(LOG_MISCONFIG, "pg: config %s: table: missing value", file_path, table);
                    return 1;
                } else if(table) {
                    log_ereport(LOG_MISCONFIG, "pg: config %s: table %s already set", file_path, table);
                    return 1;
                }
                table = value;
            } else if(xstreq(node->name, "properties")) {
                // add all child elements to the properties list
                xmlNode *ps = node->children;
                while(ps) {
                    if(ps->type == XML_ELEMENT_NODE) {
                        // validate
                        // required: namespace, value
                        if(!ps->ns || !ps->ns->href) {
                            log_ereport(LOG_MISCONFIG, "pg: config %s: property %s: missing namespace", file_path, ps->name);
                            return 1;
                        }
                        const char *value = pg_util_xml_get_text(ps);
                        if(!value) {
                            log_ereport(LOG_MISCONFIG, "pg: config %s: no column specified for property %s", file_path, ps->name);
                            return 1;
                        }
                        cxListAdd(properties, ps);
                    }
                    ps = ps->next;
                }
            }
        }
        node = node->next;
    }
    
    // check if anything is missing
    if(!table) {
        log_ereport(LOG_MISCONFIG, "pg: config %s: missing table value for extension", file_path);
        return 1;
    }
    if(!properties) {
        log_ereport(LOG_MISCONFIG, "pg: config %s: no properties configured for extension", file_path);
        return 1;
    }  
    
    // check if the table was already specified
    if(cxMapGet(ext->table_lookup, cx_hash_key_str(table))) {
        log_ereport(LOG_MISCONFIG, "pg: config %s: extension table %s not unique", file_path, table);
        return 1;
    }
    // mark table as used
    // tabname will be used later, so ist must be allocated in the pool
    char *tabname = pool_strdup(pool, table);
    if(!tabname) return 1;
    
    // exttable is only used temporarily
    PgExtTable exttable;
    exttable.table = tabname;
    exttable.isused = 0; // not relevant in config
    int tableindex = (int)ext->tables->size;
    cxListAdd(ext->tables, &exttable);
    
    if(cxMapPut(ext->table_lookup, cx_hash_key_str(table), (void*)table)) {
        return 1;
    }
     
    CxIterator iter = cxListIterator(properties);
    cx_foreach(xmlNode *, ps, iter) {
        const char *value = pg_util_xml_get_text(ps);
        
        PgPropertyStoreExt *ext_col = pool_malloc(pool, sizeof(PgPropertyStoreExt));
        if(!ext_col) {
            return 1;
        }
        ext_col->tableindex = tableindex;
        ext_col->ns = pool_strdup(pool, (const char*)ps->ns->href);
        ext_col->name = pool_strdup(pool, (const char*)ps->name);
        ext_col->column = pool_strdup(pool, (const char*)value);
        if(!ext_col->ns || !ext_col->name || !ext_col->column) {
            return 1;
        }
        
        CxHashKey key = webdav_property_key(ext_col->ns, ext_col->name);
        int err = cxMapPut(repo->prop_ext, key, ext_col);
        free((void*)key.data);
        if(err) {
            return 1;
        }
    }
    
    return 0;
}

mercurial