From 0a779ae61285ccd6a2c9534fdec8a46ba3e9cc67 Mon Sep 17 00:00:00 2001 From: index-git Date: Tue, 12 Dec 2023 15:53:00 +0100 Subject: [PATCH] WIP: Set-up GS JDBC role service --- Makefile | 13 +++ src/geoserver/layman_role_service/config.xml | 15 +++ .../layman_role_service/rolesddl.xml | 33 ++++++ .../layman_role_service/rolesdml.xml | 106 ++++++++++++++++++ src/geoserver/role_service.py | 97 ++++++++++++++++ src/layman/__init__.py | 3 - src/layman/upgrade/upgrade_v1_23.py | 8 ++ src/layman_settings.py | 12 +- src/setup_geoserver.py | 13 ++- 9 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 src/geoserver/layman_role_service/config.xml create mode 100644 src/geoserver/layman_role_service/rolesddl.xml create mode 100644 src/geoserver/layman_role_service/rolesdml.xml create mode 100644 src/geoserver/role_service.py diff --git a/Makefile b/Makefile index 2c80ed756..3ed27a92b 100644 --- a/Makefile +++ b/Makefile @@ -3,24 +3,28 @@ start-demo: mkdir -p layman_data deps/qgis/data docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml build layman layman_client timgen + docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d postgresql docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps -u root layman bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d --force-recreate postgresql qgis geoserver redis layman celery_worker flower timgen layman_client nginx start-demo-full: mkdir -p layman_data deps/qgis/data docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml build layman layman_client timgen + docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d postgresql docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps -u root layman bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d --force-recreate postgresql qgis geoserver redis layman celery_worker flower timgen layman_client micka nginx start-demo-only: mkdir -p layman_data deps/qgis/data docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml build layman layman_client timgen + docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d postgresql docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps -u root layman bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d --force-recreate --no-deps layman celery_worker flower timgen layman_client nginx start-demo-full-with-optional-deps: mkdir -p layman_data deps/qgis/data docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml build layman layman_client timgen + docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d postgresql docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps -u root layman bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d --force-recreate @@ -33,6 +37,7 @@ build-demo: upgrade-demo: mkdir -p layman_data deps/qgis/data docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml build layman layman_client timgen + docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d postgresql docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps -u root layman bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d --force-recreate --no-deps postgresql qgis geoserver redis timgen layman_client nginx docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps layman bash -c "cd src && python3 layman_flush_redis.py && python3 wait_for_deps.py && python3 standalone_upgrade.py" @@ -40,6 +45,7 @@ upgrade-demo: upgrade-demo-full: mkdir -p layman_data deps/qgis/data docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml build layman layman_client timgen + docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d postgresql docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps -u root layman bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml up -d --force-recreate --no-deps postgresql qgis geoserver redis timgen layman_client micka nginx docker compose -f docker-compose.deps.demo.yml -f docker-compose.demo.yml run --rm --no-deps layman bash -c "cd src && python3 layman_flush_redis.py && python3 wait_for_deps.py && python3 standalone_upgrade.py" @@ -55,6 +61,7 @@ deps-stop: start-dev: mkdir -p layman_data layman_data_test tmp deps/qgis/data + docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml run --rm --no-deps -u root layman_dev bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up --force-recreate -d @@ -64,6 +71,7 @@ stop-dev: start-dev-only: mkdir -p layman_data layman_data_test tmp deps/qgis/data docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml rm -fsv layman_dev celery_worker_dev flower timgen layman_client + docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml run --rm --no-deps -u root layman_dev bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up -d layman_dev celery_worker_dev flower timgen layman_client @@ -93,6 +101,7 @@ restart-celery-dev: upgrade-dev: mkdir -p layman_data layman_data_test tmp deps/qgis/data + docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml run --rm --no-deps -u root layman_dev bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up -d timgen layman_client postgresql qgis nginx-qgis geoserver redis micka docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml run --rm --no-deps layman_dev bash -c "cd src && python3 layman_flush_redis.py && python3 wait_for_deps.py && python3 standalone_upgrade.py" @@ -195,6 +204,7 @@ test: fi; docker compose -f docker-compose.deps.yml -f docker-compose.test.yml rm -f layman_test docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --no-deps layman_test bash -c "bash ensure-test-data.sh" + docker compose -f docker-compose.deps.yml -f docker-compose.test.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --no-deps -u root layman_test bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.yml -f docker-compose.test.yml up --force-recreate --no-deps -d celery_worker_test docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --name layman_test_run_1 layman_test @@ -215,6 +225,7 @@ test-separated: fi; docker compose -f docker-compose.deps.yml -f docker-compose.test.yml rm -f layman_test docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --no-deps layman_test bash -c "bash ensure-test-data.sh" + docker compose -f docker-compose.deps.yml -f docker-compose.test.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --no-deps -u root layman_test bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.yml -f docker-compose.test.yml up --force-recreate --no-deps -d celery_worker_test docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --name layman_test_run_1 -e "TEST_TYPE=$(test_type)" layman_test bash -c "bash test_separated.sh $(max_fail)" @@ -235,6 +246,7 @@ test-static: fi; docker compose -f docker-compose.deps.yml -f docker-compose.test.yml rm -f layman_test docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --no-deps layman_test bash -c "bash ensure-test-data.sh" + docker compose -f docker-compose.deps.yml -f docker-compose.test.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --no-deps -u root layman_test bash -c "cd src && python3 -B setup_geoserver.py" docker compose -f docker-compose.deps.yml -f docker-compose.test.yml up --force-recreate --no-deps -d celery_worker_test docker compose -f docker-compose.deps.yml -f docker-compose.test.yml run --rm --name layman_test_run_1 layman_test bash -c "bash test_static.sh" @@ -283,6 +295,7 @@ geoserver-exec: docker compose -f docker-compose.deps.yml exec geoserver bash geoserver-ensure-authn: + docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml up -d postgresql docker compose -f docker-compose.deps.yml -f docker-compose.dev.yml run --rm --no-deps -u root layman_dev bash -c "cd src && python3 -B setup_geoserver.py" get-current-user: diff --git a/src/geoserver/layman_role_service/config.xml b/src/geoserver/layman_role_service/config.xml new file mode 100644 index 000000000..fb228d0b7 --- /dev/null +++ b/src/geoserver/layman_role_service/config.xml @@ -0,0 +1,15 @@ + + -6e75d003:18c584ed8f6:-7fee + layman_role_service + org.geoserver.security.jdbc.JDBCRoleService + rolesddl.xml + rolesdml.xml + false + org.postgresql.Driver + xxx + xxx + xxx + false + ADMIN + GROUP_ADMIN + diff --git a/src/geoserver/layman_role_service/rolesddl.xml b/src/geoserver/layman_role_service/rolesddl.xml new file mode 100644 index 000000000..7c9336b57 --- /dev/null +++ b/src/geoserver/layman_role_service/rolesddl.xml @@ -0,0 +1,33 @@ + + + + DDL statements for role database + role_props + + create table _role_service.roles(name varchar(64) not null,parent varchar(64), primary key(name)) + + + create table _role_service.role_props(rolename varchar(64) not null,propname varchar(64) not null, propvalue varchar(2048),primary key (rolename,propname)) + + + + create table _role_service.user_roles(username varchar(128) not null, rolename varchar(64) not null, primary key(username,rolename)) + + + create index _role_service.user_roles_idx on user_roles(rolename,username) + + + create table _role_service.group_roles(groupname varchar(128) not null, rolename varchar(64) not null, primary key(groupname,rolename)) + + + create index group_roles_idx on _role_service.group_roles(rolename,groupname) + + + + + drop table _role_service.roles + drop table _role_service.role_props + drop table _role_service.user_roles + drop table _role_service.group_roles + + diff --git a/src/geoserver/layman_role_service/rolesdml.xml b/src/geoserver/layman_role_service/rolesdml.xml new file mode 100644 index 000000000..518d46413 --- /dev/null +++ b/src/geoserver/layman_role_service/rolesdml.xml @@ -0,0 +1,106 @@ + + + + DML statements for role database + + + select count(*) from _role_service.roles + + + select name,parent from _role_service.roles + + + select parent from _role_service.roles where name = ? + + + insert into _role_service.roles (name) values (?) + + + + update _role_service.roles set name=name where name = ? + + + update _role_service.roles set parent = ? where name = ? + + + update _role_service.roles set parent = null where parent = ? + + + delete from _role_service.roles where name = ? + + + delete from _role_service.roles + + + + + select rolename,propname,propvalue from _role_service.role_props + + + select propname,propvalue from _role_service.role_props where rolename = ? + + + select p.rolename,p.propname,p.propvalue from _role_service.role_props p,_role_service.user_roles u where u.rolename = p.rolename and u.username = ? + + + select p.rolename,p.propname,p.propvalue from _role_service.role_props p,_role_service.group_roles g where g.rolename = p.rolename and g.groupname = ? + + + delete from _role_service.role_props where rolename=? + + + insert into _role_service.role_props(rolename,propname,propvalue) values (?,?,?) + + + delete from _role_service.role_props + + + + + select u.rolename,r.parent from _role_service.user_roles u ,_role_service.roles r where r.name=u.rolename and u.username = ? + + + select username from _role_service.user_roles where rolename = ? + + + insert into _role_service.user_roles(rolename,username) values (?,?) + + + delete from _role_service.user_roles where rolename=? and username = ? + + + delete from _role_service.user_roles where rolename=? + + + delete from _role_service.user_roles where username = ? + + + delete from _role_service.user_roles + + + + + + select g.rolename,r.parent from _role_service.group_roles g,r_role_service.oles r where g.rolename = r.name and g.groupname = ? + + + select groupname from _role_service.group_roles where rolename = ? + + + insert into _role_service.group_roles(rolename,groupname) values (?,?) + + + delete from _role_service.group_roles where rolename=? and groupname = ? + + + delete from _role_service.group_roles where rolename=? + + + delete from _role_service.group_roles where groupname = ? + + + delete from _role_service.group_roles + + + + diff --git a/src/geoserver/role_service.py b/src/geoserver/role_service.py new file mode 100644 index 000000000..46115c22a --- /dev/null +++ b/src/geoserver/role_service.py @@ -0,0 +1,97 @@ +from distutils.dir_util import copy_tree +import logging +import os +import shutil +import sys +import time +from urllib.parse import urlparse +from lxml import etree as ET + +from db import util as db_util +from requests_util import url_util +from . import authn + +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + + +ROLE_SERVICE_PATH = 'security/role/' +DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + + +def wait_for_db(conn_dict): + max_attempts = 10 + attempt = 0 + + while True: + import psycopg2 + try: + with psycopg2.connect(**conn_dict): + pass + logger.info(f" Attempt {attempt}/{max_attempts} successful.") + break + except psycopg2.OperationalError: + if attempt >= max_attempts: + logger.info(f" Reaching max attempts when waiting for DB") + sys.exit(1) + time.sleep(2) + attempt += 1 + + +def setup_role_service(data_dir, db_conn, uri_str, internal_service_schema, layman_pg_user, layman_gs_user, layman_gs_role, service_url, role_service_name): + logger.info(f"Ensuring GeoServer DB role service '{role_service_name}' " + f"for URL: {service_url}.") + + logger.info(f" Waiting for DB") + wait_for_db(db_conn) + + logger.info(f" Checking internal role service DB schema") + schema_query = f'''SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '{internal_service_schema}';''' + schema_exists = db_util.run_query(schema_query, uri_str=uri_str)[0][0] + if schema_exists == 0: + logger.info(f" Setting up internal role service DB schema") + statement = f""" + CREATE SCHEMA "{internal_service_schema}" AUTHORIZATION {layman_pg_user}; + create view {internal_service_schema}.roles as select 'ADMIN' as name, null as parent + union all select 'GROUP_ADMIN', null + union all select 'LAYMAN_ROLE', null + ; + create view {internal_service_schema}.role_props as select null::varchar as rolename, null::varchar as propname, null::varchar as propvalue; + create view {internal_service_schema}.user_roles as select 'layman' as username, 'ADMIN' as rolename + union all select 'layman', 'LAYMAN_ROLE' + union all select 'admin', 'ADMIN' + ; + create view {internal_service_schema}.group_roles as select null::varchar as groupname, null::varchar as rolename; +""" + db_util.run_statement(statement, uri_str=uri_str) + + logger.info(f" Setting up files") + role_service_path = os.path.join(data_dir, ROLE_SERVICE_PATH) + layman_role_service_path = os.path.join(role_service_path, role_service_name) + if os.path.exists(layman_role_service_path): + shutil.rmtree(layman_role_service_path) + source_path = os.path.join(DIRECTORY, role_service_name) + os.mkdir(layman_role_service_path) + copy_tree(source_path, layman_role_service_path) + + role_service_config_path = os.path.join(layman_role_service_path, 'config.xml') + role_service_xml = ET.parse(role_service_config_path) + + parsed_url = urlparse(service_url) + + element = role_service_xml.find('userName') + element.text = parsed_url.username + + element = role_service_xml.find('password') + element.text = parsed_url.password + + element = role_service_xml.find('connectURL') + element.text = f'jdbc:{url_util.redact_uri(service_url, remove_username=True)}' + + role_service_xml.write(role_service_config_path) + + security_xml = authn.get_security(data_dir) + element = security_xml.find('roleServiceName') + element.text = role_service_name + security_path = os.path.join(data_dir, 'security/config.xml') + security_xml.write(security_path) diff --git a/src/layman/__init__.py b/src/layman/__init__.py index a76a05daa..e5c578b7f 100644 --- a/src/layman/__init__.py +++ b/src/layman/__init__.py @@ -86,10 +86,7 @@ logger.info(f'Adjusting GeoServer roles') if settings.GEOSERVER_ADMIN_AUTH: - gs_util.ensure_role(settings.LAYMAN_GS_ROLE, settings.GEOSERVER_ADMIN_AUTH) gs_util.ensure_user(settings.LAYMAN_GS_USER, settings.LAYMAN_GS_PASSWORD, settings.GEOSERVER_ADMIN_AUTH) - gs_util.ensure_user_role(settings.LAYMAN_GS_USER, 'ADMIN', settings.GEOSERVER_ADMIN_AUTH) - gs_util.ensure_user_role(settings.LAYMAN_GS_USER, settings.LAYMAN_GS_ROLE, settings.GEOSERVER_ADMIN_AUTH) gs_util.ensure_proxy_base_url(settings.LAYMAN_GS_PROXY_BASE_URL_WITH_PLACEHOLDERS, settings.LAYMAN_GS_AUTH) diff --git a/src/layman/upgrade/upgrade_v1_23.py b/src/layman/upgrade/upgrade_v1_23.py index 237c11a99..a98db7bd7 100644 --- a/src/layman/upgrade/upgrade_v1_23.py +++ b/src/layman/upgrade/upgrade_v1_23.py @@ -34,6 +34,14 @@ def create_role_service_schema(): create_schema = f"""CREATE SCHEMA IF NOT EXISTS "{ROLE_SERVICE_SCHEMA}" AUTHORIZATION {settings.LAYMAN_PG_USER};""" db_util.run_statement(create_schema) + drop_temporary_views = f""" +drop view {ROLE_SERVICE_SCHEMA}.roles; +drop view {ROLE_SERVICE_SCHEMA}.role_props; +drop view {ROLE_SERVICE_SCHEMA}.user_roles; +drop view {ROLE_SERVICE_SCHEMA}.group_roles; +""" + db_util.run_statement(drop_temporary_views) + create_role_table = f"""create table {ROLE_SERVICE_SCHEMA}.bussiness_roles( id integer GENERATED ALWAYS AS IDENTITY, name varchar(64) not null, diff --git a/src/layman_settings.py b/src/layman_settings.py index 27dbe0593..4ccfaa9dc 100644 --- a/src/layman_settings.py +++ b/src/layman_settings.py @@ -105,7 +105,12 @@ class EnumWfsWmsStatus(Enum): GEOSERVER_ADMIN_PASSWORD) GEOSERVER_DATADIR = '/geoserver/data_dir' GEOSERVER_INITIAL_DATADIR = '/geoserver/initial_data_dir' -LAYMAN_GS_ROLE_SERVICE = os.getenv('LAYMAN_GS_ROLE_SERVICE', '') or 'default' +LAYMAN_GS_ROLE_SERVICE = 'layman_role_service' +# Name of schema, where Layman maintains internal GS JDBC Role Service. +LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA = '_role_service' +LAYMAN_ROLE_SERVICE_URI = os.environ['LAYMAN_ROLE_SERVICE_URI'] +LAYMAN_ROLE_SERVICE_SCHEMA = parse_qs(urlparse(LAYMAN_ROLE_SERVICE_URI).query)['schema'][0] + LAYMAN_GS_USER_GROUP_SERVICE = os.getenv('LAYMAN_GS_USER_GROUP_SERVICE', '') or 'default' LAYMAN_GS_USER = os.environ['LAYMAN_GS_USER'] @@ -223,11 +228,6 @@ class EnumWfsWmsStatus(Enum): if RIGHTS_EVERYONE_ROLE not in GRANT_PUBLISH_IN_PUBLIC_WORKSPACE: assert not GRANT_CREATE_PUBLIC_WORKSPACE.difference(GRANT_PUBLISH_IN_PUBLIC_WORKSPACE) -# Name of schema, where Layman maintains internal GS JDBC Role Service. -LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA = '_role_service' -LAYMAN_ROLE_SERVICE_URI = os.environ['LAYMAN_ROLE_SERVICE_URI'] -LAYMAN_ROLE_SERVICE_SCHEMA = parse_qs(urlparse(LAYMAN_ROLE_SERVICE_URI).query)['schema'][0] - # UPLOAD_MAX_INACTIVITY_TIME = 10 # 10 seconds UPLOAD_MAX_INACTIVITY_TIME = 5 * 60 # 5 minutes diff --git a/src/setup_geoserver.py b/src/setup_geoserver.py index 5010e3a37..d7603ea0e 100644 --- a/src/setup_geoserver.py +++ b/src/setup_geoserver.py @@ -2,8 +2,7 @@ import sys import geoserver -from geoserver import epsg_properties -from geoserver import authn +from geoserver import epsg_properties, authn, role_service import layman_settings as settings @@ -26,6 +25,16 @@ def main(): ) epsg_properties.setup_epsg(settings.GEOSERVER_DATADIR, set(settings.LAYMAN_OUTPUT_SRS_LIST)) + role_service.setup_role_service(settings.GEOSERVER_DATADIR, + settings.PG_CONN, + settings.PG_URI_STR, + settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA, + settings.LAYMAN_PG_USER, + settings.LAYMAN_GS_USER, + settings.LAYMAN_GS_ROLE, + settings.LAYMAN_ROLE_SERVICE_URI, + settings.LAYMAN_GS_ROLE_SERVICE, + ) if __name__ == "__main__":