diff --git a/CHANGELOG.md b/CHANGELOG.md index f795a0ab6..fe1a4326d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Upgrade requirements ### Changes - [#28](https://github.com/jirik/layman/issues/28) New environment variable [LAYMAN_PRIME_SCHEMA](doc/env-settings.md#LAYMAN_PRIME_SCHEMA). +- [#28](https://github.com/jirik/layman/issues/28) New REST endpoint [GET Users](doc/rest.md#get-users) with list of all users registered in Layman. ## v1.7.1 2020-09-30 diff --git a/doc/rest.md b/doc/rest.md index 47f3df813..f5c69d2e0 100644 --- a/doc/rest.md +++ b/doc/rest.md @@ -14,6 +14,7 @@ |Map File|`/rest//maps//file`|[GET](#get-map-file)| x | x | x | |Map Thumbnail|`/rest//maps//thumbnail`|[GET](#get-map-thumbnail)| x | x | x | |Map Metadata Comparison|`/rest//layers//metadata-comparison`|[GET](#get-map-metadata-comparison) | x | x | x | +|Users|`/rest/users`|[GET](#get-users)| x | x | x | |Current [User](models.md#user)|`/rest/current-user`|[GET](#get-current-user)| x | [PATCH](#patch-current-user) | [DELETE](#delete-current-user) | #### REST path parameters @@ -490,6 +491,26 @@ JSON object with one attribute: - **equal_or_null**: Boolean. True if all values are considered equal or null, false otherwise. +## Users +### URL +`/rest/users` + +### GET Users +Get list of registered users. + +#### Request. +No action parameters. + +#### Response +Content-Type: `application/json` + +JSON array of objects representing users of Layman with following structure: +- **username**: String. Username of the user. +- **given_name**: String. Given name of the user. +- **family_name**: String. Family name of the user +- **middle_name**: String. Middle name of the user +- **name**: String. Whole name of the user (given_name + middle_name + family_name). + ## Current User ### URL `/rest/current-user` diff --git a/src/layman/__init__.py b/src/layman/__init__.py index c1cd39e0e..898cd572d 100644 --- a/src/layman/__init__.py +++ b/src/layman/__init__.py @@ -5,7 +5,6 @@ import sys import time - IN_CELERY_WORKER_PROCESS = sys.argv and sys.argv[0].endswith('/celery/__main__.py') IN_PYTEST_PROCESS = sys.argv and sys.argv[0].endswith('/pytest/__main__.py') IN_FLOWER_PROCESS = sys.argv and sys.argv[0].endswith('/flower/__main__.py') @@ -35,10 +34,11 @@ from .user.rest_current_user import bp as current_user_bp from .gs_wfs_proxy import bp as gs_wfs_proxy_bp -from .common import prime_db_schema as db_util +from .user.rest_users import bp as users_bp app.register_blueprint(current_user_bp, url_prefix='/rest/current-user') app.register_blueprint(gs_wfs_proxy_bp, url_prefix='/geoserver') +app.register_blueprint(users_bp, url_prefix=f'/rest/{settings.REST_USERS_PREFIX}') app.logger.info(f"IN_CELERY_WORKER_PROCESS={IN_CELERY_WORKER_PROCESS}") app.logger.info(f"IN_PYTEST_PROCESS={IN_PYTEST_PROCESS}") @@ -64,8 +64,11 @@ ensure_proxy_base_url(settings.LAYMAN_GS_PROXY_BASE_URL, settings.LAYMAN_GS_AUTH) with app.app_context(): - db_util.check_schema_name() - db_util.ensure_schema() + import layman.common.prime_db_schema.schema_initialization as prime_db_schema + prime_db_schema.check_schema_name(settings.LAYMAN_PRIME_SCHEMA) + prime_db_schema.ensure_schema(settings.LAYMAN_PRIME_SCHEMA, + app, + settings.PUBLICATION_MODULES) app.logger.info(f'Loading Redis database') with app.app_context(): diff --git a/src/layman/authn/prime_db_schema.py b/src/layman/authn/prime_db_schema.py new file mode 100644 index 000000000..11cfa7a9f --- /dev/null +++ b/src/layman/authn/prime_db_schema.py @@ -0,0 +1,9 @@ +from layman.common.prime_db_schema import ensure_whole_user + + +def save_username_reservation(username, iss_id, sub, claims): + userinfo = {"iss_id": iss_id, + "sub": sub, + "claims": claims, + } + ensure_whole_user(username, userinfo) diff --git a/src/layman/common/prime_db_schema/__init__.py b/src/layman/common/prime_db_schema/__init__.py index ba009d752..906a0face 100644 --- a/src/layman/common/prime_db_schema/__init__.py +++ b/src/layman/common/prime_db_schema/__init__.py @@ -1,56 +1,23 @@ -from layman.util import get_usernames, get_modules_from_names, call_modules_fn -from layman.common.util import merge_infos -from layman import settings, app, LaymanError -from layman.authz.util import get_publication_access_rights -from layman.common.prime_db_schema import publications, model, users -from layman.common.prime_db_schema.util import run_query, run_statement - - -DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA - - -def get_default_everyone_can_read(): - return model.BOOLEAN_TRUE - - -def get_default_everyone_can_write(): - return get_publication_access_rights('', '', '')["guest"] == "w" - - -def migrate_users_and_publications(): - usernames = get_usernames(use_cache=False) - - for username in usernames: - users.ensure_user(username) - for publ_module in get_modules_from_names(settings.PUBLICATION_MODULES): - for type_def in publ_module.PUBLICATION_TYPES.values(): - publ_type_name = type_def['type'] - sources = get_modules_from_names(type_def['internal_sources']) - results = call_modules_fn(sources, 'get_publication_infos', [username, publ_type_name]) - pubs = merge_infos(results) - for name, info in pubs.items(): - pub_info = {"name": name, - "title": info.get("title"), - "publ_type_name": publ_type_name, - "uuid": info.get("uuid"), - "can_read": set(), - "can_write": set(), - } - publications.insert_publication(username, pub_info) - - -def check_schema_name(): - usernames = get_usernames(use_cache=False, skip_modules=('layman.map.prime_db_schema', 'layman.layer.prime_db_schema', )) - if DB_SCHEMA in usernames: - raise LaymanError(42, {'username': DB_SCHEMA}) - - -def ensure_schema(): - exists_schema = run_query(model.EXISTS_SCHEMA_SQL) - if exists_schema[0][0] == 0: - app.logger.info(f"Going to create Layman DB schema, schema_name={DB_SCHEMA}") - run_statement(model.CREATE_SCHEMA_SQL) - run_statement(model.setup_codelists_data()) - migrate_users_and_publications() - else: - app.logger.info(f"Layman DB schema already exists, schema_name={DB_SCHEMA}") +from layman.common.prime_db_schema import users as users_util, workspaces as workspaces_util + +get_usernames = workspaces_util.get_workspace_names + + +def check_new_layername(username, layername): + pass + + +def delete_whole_user(username): + users_util.delete_user(username) + workspaces_util.delete_workspace(username) + + +def ensure_whole_user(username, userinfo=None): + id_workspace = workspaces_util.ensure_workspace(username) + if userinfo: + users_util.ensure_user(id_workspace, userinfo) + + +def check_username(username): + users_util.check_username(username) + workspaces_util.check_workspace_name(username) diff --git a/src/layman/common/prime_db_schema/migrate_test.py b/src/layman/common/prime_db_schema/migrate_test.py index d5ba81284..bc2029665 100644 --- a/src/layman/common/prime_db_schema/migrate_test.py +++ b/src/layman/common/prime_db_schema/migrate_test.py @@ -1,7 +1,8 @@ import test.flask_client as client_util from layman import settings, app as app -from . import ensure_schema, migrate_users_and_publications, model, publications +from . import model, publications as pub_util, workspaces as workspaces_util +from .schema_initialization import migrate_users_and_publications, ensure_schema from .util import run_query, run_statement from layman import util from layman.layer import util as layer_util @@ -18,7 +19,9 @@ def test_recreate_schema(client): with app.app_context(): run_statement(model.DROP_SCHEMA_SQL) - ensure_schema() + ensure_schema(settings.LAYMAN_PRIME_SCHEMA, + app, + settings.PUBLICATION_MODULES) client_util.delete_layer(username, 'test_recreate_schema_layer1', client) client_util.delete_map(username, 'test_recreate_schema_map1', client) @@ -32,17 +35,26 @@ def test_recreate_schema(client): def test_schema(client): username = 'migration_test_user1' - client_util.publish_layer(username, 'migration_test_layer1', client) - client_util.publish_map(username, 'migration_test_map1', client) + layername = 'migration_test_layer1' + mapname = 'migration_test_map1' + client_util.publish_layer(username, layername, client) + client_util.publish_map(username, mapname, client) with app.app_context(): run_statement(model.DROP_SCHEMA_SQL) - ensure_schema() - users = run_query(f'select count(*) from {DB_SCHEMA}.users;') - assert users[0][0] == len(util.get_usernames()) - - client_util.delete_layer(username, 'migration_test_layer1', client) - client_util.delete_map(username, 'migration_test_map1', client) + ensure_schema(settings.LAYMAN_PRIME_SCHEMA, + app, + settings.PUBLICATION_MODULES) + workspaces = run_query(f'select count(*) from {DB_SCHEMA}.workspaces;') + assert workspaces[0][0] == len(util.get_usernames()) + user_infos = workspaces_util.get_workspace_infos(username) + assert username in user_infos + pub_infos = pub_util.get_publication_infos(username) + assert layername in pub_infos + assert mapname in pub_infos + + client_util.delete_layer(username, layername, client) + client_util.delete_map(username, mapname, client) with app.app_context(): pubs = layer_util.get_layer_infos(username) @@ -71,13 +83,13 @@ def test_steps(client): exists_right_types = run_query(f'select count(*) from {DB_SCHEMA}.right_types;') assert exists_right_types[0][0] == 2 - exists_users = run_query(f'select count(*) from {DB_SCHEMA}.users;') - assert exists_users[0][0] == 0 + exists_workspaces = run_query(f'select count(*) from {DB_SCHEMA}.workspaces;') + assert exists_workspaces[0][0] == 0 exists_pubs = run_query(f'select count(*) from {DB_SCHEMA}.publications;') assert exists_pubs[0][0] == 0 - migrate_users_and_publications() - exists_users = run_query(f'select count(*) from {DB_SCHEMA}.users;') - assert exists_users[0][0] > 0 + migrate_users_and_publications(settings.PUBLICATION_MODULES) + exists_workspaces = run_query(f'select count(*) from {DB_SCHEMA}.workspaces;') + assert exists_workspaces[0][0] > 0 exists_pubs = run_query(f'select count(*) from {DB_SCHEMA}.publications;') assert exists_pubs[0][0] > 0 diff --git a/src/layman/common/prime_db_schema/model.py b/src/layman/common/prime_db_schema/model.py index 191a4e0e7..80db12f8f 100644 --- a/src/layman/common/prime_db_schema/model.py +++ b/src/layman/common/prime_db_schema/model.py @@ -69,6 +69,15 @@ def setup_codelists_data(): ALTER SEQUENCE {DB_SCHEMA}.users_id_seq OWNER TO {settings.LAYMAN_PG_USER}; +CREATE SEQUENCE {DB_SCHEMA}.workspaces_id_seq + INCREMENT 1 + START 1 + MINVALUE 1 + MAXVALUE 2147483647 + CACHE 1; +ALTER SEQUENCE {DB_SCHEMA}.workspaces_id_seq + OWNER TO {settings.LAYMAN_PG_USER}; + CREATE SEQUENCE {DB_SCHEMA}.rights_id_seq INCREMENT 1 START 1 @@ -96,19 +105,35 @@ def setup_codelists_data(): ) TABLESPACE pg_default; +CREATE TABLE {DB_SCHEMA}.workspaces +( + id integer NOT NULL DEFAULT nextval('{DB_SCHEMA}.workspaces_id_seq'::regclass), + name VARCHAR(256) COLLATE pg_catalog."default", + CONSTRAINT workspaces_pkey PRIMARY KEY (id), + CONSTRAINT workspaces_name_key UNIQUE (name) +) +TABLESPACE pg_default; + CREATE TABLE {DB_SCHEMA}.users ( id integer NOT NULL DEFAULT nextval('{DB_SCHEMA}.users_id_seq'::regclass), - username VARCHAR(256) COLLATE pg_catalog."default" not null, + id_workspace integer REFERENCES {DB_SCHEMA}.workspaces (id), + given_name VARCHAR(256) COLLATE pg_catalog."default", + family_name VARCHAR(256) COLLATE pg_catalog."default", + middle_name VARCHAR(256) COLLATE pg_catalog."default", + name VARCHAR(256) COLLATE pg_catalog."default", + email VARCHAR(1024) COLLATE pg_catalog."default", + issuer_id VARCHAR(256) COLLATE pg_catalog."default", + sub VARCHAR(256) COLLATE pg_catalog."default", CONSTRAINT users_pkey PRIMARY KEY (id), - CONSTRAINT users_username_key UNIQUE (username) + CONSTRAINT users_workspace_key UNIQUE (id_workspace) ) TABLESPACE pg_default; CREATE TABLE {DB_SCHEMA}.publications ( id integer NOT NULL DEFAULT nextval('{DB_SCHEMA}.publications_id_seq'::regclass), - id_user integer NOT NULL REFERENCES {DB_SCHEMA}.users (id), + id_workspace integer REFERENCES {DB_SCHEMA}.workspaces (id), name VARCHAR(256) COLLATE pg_catalog."default" not null, title VARCHAR(256) COLLATE pg_catalog."default" not null, type VARCHAR(64) COLLATE pg_catalog."default" not null references {DB_SCHEMA}.publication_types (name), @@ -117,7 +142,7 @@ def setup_codelists_data(): everyone_can_write boolean not null, constraint publications_pkey primary key (id), constraint publications_uuid_key unique (uuid), - constraint publications_name_type_key unique (id_user, type, name) + constraint publications_name_type_key unique (id_workspace, type, name) ) TABLESPACE pg_default; diff --git a/src/layman/common/prime_db_schema/publications.py b/src/layman/common/prime_db_schema/publications.py index 25a37df90..b78667670 100644 --- a/src/layman/common/prime_db_schema/publications.py +++ b/src/layman/common/prime_db_schema/publications.py @@ -1,5 +1,5 @@ -from . import util, users -from layman import settings +from . import util, workspaces +from layman import settings, app DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA ROLE_EVERYONE = settings.RIGHTS_EVERYONE_ROLE @@ -7,29 +7,31 @@ def get_publication_infos(username=None, pub_type=None): sql = f"""with const as (select %s username, %s pub_type) -select u.username, +select w.name as username, p.type, p.name, p.title, p.uuid, p.everyone_can_read, p.everyone_can_write, - (select COALESCE(string_agg(u2.username, ', '), '') + (select COALESCE(string_agg(w.name, ', '), '') from {DB_SCHEMA}.rights r inner join - {DB_SCHEMA}.users u2 on r.id_user = u2.id + {DB_SCHEMA}.users u on r.id_user = u.id inner join + {DB_SCHEMA}.workspaces w on w.id = u.id_workspace where r.id_publication = p.id and r.type = 'read') can_read_users, - (select COALESCE(string_agg(u2.username, ', '), '') + (select COALESCE(string_agg(w.name, ', '), '') from {DB_SCHEMA}.rights r inner join - {DB_SCHEMA}.users u2 on r.id_user = u2.id + {DB_SCHEMA}.users u on r.id_user = u.id inner join + {DB_SCHEMA}.workspaces w on w.id = u.id_workspace where r.id_publication = p.id and r.type = 'write') can_write_users from const c inner join - {DB_SCHEMA}.users u on ( u.username = c.username - or c.username is null) inner join - {DB_SCHEMA}.publications p on p.id_user = u.id - and ( p.type = c.pub_type - or c.pub_type is null) + {DB_SCHEMA}.workspaces w on ( w.name = c.username + or c.username is null) inner join + {DB_SCHEMA}.publications p on p.id_workspace = w.id + and ( p.type = c.pub_type + or c.pub_type is null) ;""" values = util.run_query(sql, (username, pub_type,)) infos = {layername: {'name': layername, @@ -45,14 +47,14 @@ def get_publication_infos(username=None, pub_type=None): def insert_publication(username, info): - user_id = users.ensure_user(username) + id_workspace = workspaces.ensure_workspace(username) insert_publications_sql = f'''insert into {DB_SCHEMA}.publications as p - (id_user, name, title, type, uuid, everyone_can_read, everyone_can_write) values + (id_workspace, name, title, type, uuid, everyone_can_read, everyone_can_write) values (%s, %s, %s, %s, %s, %s, %s) returning id ;''' - data = (user_id, + data = (id_workspace, info.get("name"), info.get("title"), info.get("publ_type_name"), @@ -65,12 +67,12 @@ def insert_publication(username, info): def update_publication(username, info): - user_id = users.get_user_infos(username)[username]["id"] + id_workspace = workspaces.get_workspace_infos(username)[username]["id"] insert_publications_sql = f'''update {DB_SCHEMA}.publications set title = coalesce(%s, title), everyone_can_read = coalesce(%s, everyone_can_read), everyone_can_write = coalesce(%s, everyone_can_write) -where id_user = %s +where id_workspace = %s and name = %s and type = %s returning id @@ -79,7 +81,7 @@ def update_publication(username, info): data = (info.get("title"), ROLE_EVERYONE in (info.get("can_read") or set()), ROLE_EVERYONE in (info.get("can_write") or set()), - user_id, + id_workspace, info.get("name"), info.get("publ_type_name"), ) @@ -88,8 +90,12 @@ def update_publication(username, info): def delete_publication(username, name, type): - user_id = users.ensure_user(username) - sql = f"""delete from {DB_SCHEMA}.publications p where p.id_user = %s and p.name = %s and p.type = %s;""" - util.run_statement(sql, (user_id, - name, - type,)) + workspace_info = workspaces.get_workspace_infos(username).get(username) + if workspace_info: + id_workspace = workspace_info["id"] + sql = f"""delete from {DB_SCHEMA}.publications p where p.id_workspace = %s and p.name = %s and p.type = %s;""" + util.run_statement(sql, (id_workspace, + name, + type,)) + else: + app.logger.warning(f'Deleting publication for NON existing workspace. workspace_name={username}, pub_name={name}, type={type}') diff --git a/src/layman/common/prime_db_schema/publications_test.py b/src/layman/common/prime_db_schema/publications_test.py index f4f541363..239f605c1 100644 --- a/src/layman/common/prime_db_schema/publications_test.py +++ b/src/layman/common/prime_db_schema/publications_test.py @@ -5,7 +5,7 @@ from layman.layer import LAYER_TYPE from layman.map.filesystem import uuid as map_uuid from layman.map import MAP_TYPE -from . import publications, users +from . import publications, workspaces DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA ROLE_EVERYONE = publications.ROLE_EVERYONE @@ -19,7 +19,7 @@ def test_post_layer(client): layertitle = 'test_post_layer_layer Title' layertitle2 = 'test_post_layer_layer Title2' with app.app_context(): - users.ensure_user(username) + workspaces.ensure_workspace(username) uuid_str = layer_uuid.assign_layer_uuid(username, layername) db_info = {"name": layername, "title": layertitle, @@ -74,7 +74,7 @@ def test_post_map(client): maptitle = 'test_post_map_map Title' maptitle2 = 'test_post_map_map Title2' with app.app_context(): - users.ensure_user(username) + workspaces.ensure_workspace(username) uuid_str = map_uuid.assign_map_uuid(username, mapname) db_info = {"name": mapname, "title": maptitle, @@ -127,8 +127,6 @@ def test_select_publications(client): username = 'test_select_publications_user1' layername = 'test_select_publications_layer1' mapname = 'test_select_publications_map1' - with app.app_context(): - users.ensure_user(username) client_util.publish_layer(username, layername, client) client_util.publish_map(username, mapname, client) diff --git a/src/layman/common/prime_db_schema/schema_initialization.py b/src/layman/common/prime_db_schema/schema_initialization.py new file mode 100644 index 000000000..7858319f4 --- /dev/null +++ b/src/layman/common/prime_db_schema/schema_initialization.py @@ -0,0 +1,48 @@ +from layman.http import LaymanError +from layman.authn.filesystem import get_authn_info +from layman.common.prime_db_schema import workspaces, users, publications +from layman.common.util import merge_infos +from layman.util import get_modules_from_names, call_modules_fn, get_usernames as global_get_usernames +from . import util as db_util, model + + +def migrate_users_and_publications(modules): + usernames = global_get_usernames(use_cache=False) + + for username in usernames: + userinfo = get_authn_info(username) + id_workspace = workspaces.ensure_workspace(username) + if userinfo != {}: + users.ensure_user(id_workspace, userinfo) + for publ_module in get_modules_from_names(modules): + for type_def in publ_module.PUBLICATION_TYPES.values(): + publ_type_name = type_def['type'] + sources = get_modules_from_names(type_def['internal_sources']) + results = call_modules_fn(sources, 'get_publication_infos', [username, publ_type_name]) + pubs = merge_infos(results) + for name, info in pubs.items(): + pub_info = {"name": name, + "title": info.get("title"), + "publ_type_name": publ_type_name, + "uuid": info.get("uuid"), + "can_read": set(), + "can_write": set(), + } + publications.insert_publication(username, pub_info) + + +def ensure_schema(db_schema, app, modules): + exists_schema = db_util.run_query(model.EXISTS_SCHEMA_SQL) + if exists_schema[0][0] == 0: + app.logger.info(f"Going to create Layman DB schema, schema_name={db_schema}") + db_util.run_statement(model.CREATE_SCHEMA_SQL) + db_util.run_statement(model.setup_codelists_data()) + migrate_users_and_publications(modules) + else: + app.logger.info(f"Layman DB schema already exists, schema_name={db_schema}") + + +def check_schema_name(db_schema): + usernames = global_get_usernames(use_cache=False, skip_modules=('layman.map.prime_db_schema', 'layman.layer.prime_db_schema', )) + if db_schema in usernames: + raise LaymanError(42, {'username': db_schema}) diff --git a/src/layman/common/prime_db_schema/users.py b/src/layman/common/prime_db_schema/users.py index b720a6e02..ce32e0009 100644 --- a/src/layman/common/prime_db_schema/users.py +++ b/src/layman/common/prime_db_schema/users.py @@ -1,32 +1,59 @@ -from . import util -from layman.http import LaymanError +from . import util, workspaces from layman import settings DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA -def ensure_user(username): - sql = f"""insert into {DB_SCHEMA}.users (username) values (%s) -ON CONFLICT (username) DO update SET username = EXCLUDED.username returning id;""" - ids = util.run_query(sql, (username, )) +def ensure_user(id_workspace, userinfo): + sql = f"""insert into {DB_SCHEMA}.users (id_workspace, given_name, family_name, middle_name, name, email, issuer_id, sub) +values (%s, %s, %s, %s, %s, %s, %s, %s) +ON CONFLICT (id_workspace) DO update SET id_workspace = EXCLUDED.id_workspace returning id;""" + data = (id_workspace, + userinfo["claims"]["given_name"], + userinfo["claims"]["family_name"], + userinfo["claims"]["middle_name"], + userinfo["claims"]["name"], + userinfo["claims"]["email"], + userinfo["iss_id"], + userinfo["sub"], + ) + ids = util.run_query(sql, data) return ids[0][0] def delete_user(username): sql = f"delete from {DB_SCHEMA}.users where username = %s;" - util.run_statement(sql, (username, )) + util.run_statement(sql, (username,)) def get_user_infos(username=None): sql = f"""with const as (select %s username) -select u.id, u.username -from {DB_SCHEMA}.users u inner join - const c on ( c.username = u.username +select u.id, + w.name username, + u.given_name, + u.family_name, + u.middle_name, + u.name, + u.email, + u.issuer_id, + u.sub +from {DB_SCHEMA}.workspaces w inner join + {DB_SCHEMA}.users u on w.id = u.id_workspace inner join + const c on ( c.username = w.name or c.username is null) +order by w.name asc ;""" - values = util.run_query(sql, (username, )) + values = util.run_query(sql, (username,)) result = {username: {"id": user_id, - "username": username} for user_id, username in values} + "username": username, + "given_name": given_name, + "family_name": family_name, + "middle_name": middle_name, + "name": name, + "email": email, + "issuer_id": issuer_id, + "sub": sub, + } for user_id, username, given_name, family_name, middle_name, name, email, issuer_id, sub in values} return result diff --git a/src/layman/common/prime_db_schema/users_test.py b/src/layman/common/prime_db_schema/users_test.py index c8cdb138b..d2bd35c13 100644 --- a/src/layman/common/prime_db_schema/users_test.py +++ b/src/layman/common/prime_db_schema/users_test.py @@ -1,22 +1,32 @@ from test.flask_client import client from layman import settings, app as app -from . import users as user_util +from . import users as user_util, workspaces as workspace_util DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA def test_get_user_infos(client): with app.app_context(): - users = user_util.get_user_infos() - users = user_util.get_user_infos('test2') - users = user_util.get_user_infos('asůldghwíeghsdlkfj') + user_util.get_user_infos() + user_util.get_user_infos('test2') + user_util.get_user_infos('asůldghwíeghsdlkfj') def test_ensure_user(client): username = 'test_ensure_user' + userinfo = {"iss_id": 'mock_test', + "sub": '1', + "claims": {"email": "test@liferay.com", + "name": "test ensure user", + "given_name": "test", + "family_name": "user", + "middle_name": "ensure", + } + } with app.app_context(): - user_id = user_util.ensure_user(username) + id_workspace = workspace_util.ensure_workspace(username) + user_id = user_util.ensure_user(id_workspace, userinfo) assert user_id - user_id2 = user_util.ensure_user(username) + user_id2 = user_util.ensure_user(id_workspace, userinfo) assert user_id2 == user_id diff --git a/src/layman/common/prime_db_schema/util.py b/src/layman/common/prime_db_schema/util.py index 67b1d028c..a58dad323 100644 --- a/src/layman/common/prime_db_schema/util.py +++ b/src/layman/common/prime_db_schema/util.py @@ -1,7 +1,7 @@ -from flask import g +from flask import g, current_app as app import psycopg2 -from layman import settings, app +from layman import settings from layman.http import LaymanError diff --git a/src/layman/common/prime_db_schema/workspaces.py b/src/layman/common/prime_db_schema/workspaces.py new file mode 100644 index 000000000..091b76a4c --- /dev/null +++ b/src/layman/common/prime_db_schema/workspaces.py @@ -0,0 +1,43 @@ +from . import util +from layman import settings + +DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA + + +def ensure_workspace(name): + sql = f"""insert into {DB_SCHEMA}.workspaces (name) + values (%s) + ON CONFLICT (name) DO update SET name = EXCLUDED.name returning id;""" + data = (name, ) + ids = util.run_query(sql, data) + return ids[0][0] + + +def delete_workspace(name): + sql = f"delete from {DB_SCHEMA}.workspaces where name = %s;" + util.run_statement(sql, (name,)) + + +def get_workspace_infos(name=None): + sql = f"""with const as (select %s as name) + select w.id, + w.name + from {DB_SCHEMA}.workspaces w inner join + const c on ( c.name = w.name + or c.name is null) + order by w.name asc + ;""" + values = util.run_query(sql, (name,)) + result = {name: {"id": workspace_id, + "name": name, + } for workspace_id, name in + values} + return result + + +def get_workspace_names(): + return get_workspace_infos().keys() + + +def check_workspace_name(name): + pass diff --git a/src/layman/common/prime_db_schema/workspaces_test.py b/src/layman/common/prime_db_schema/workspaces_test.py new file mode 100644 index 000000000..62445649a --- /dev/null +++ b/src/layman/common/prime_db_schema/workspaces_test.py @@ -0,0 +1,40 @@ +from test.flask_client import client + +from layman import app as app +from . import users as user_util, workspaces as workspace_util, ensure_whole_user + + +def test_get_workspace_infos(client): + with app.app_context(): + workspace_util.get_workspace_infos() + workspace_util.get_workspace_infos('test2') + workspace_util.get_workspace_infos('asůldghwíeghsdlkfj') + + +def test_ensure_workspace(client): + username = 'test_ensure_workspace_user' + + with app.app_context(): + id_workspace = workspace_util.ensure_workspace(username) + assert id_workspace + id_user = user_util.get_user_infos(username) + assert not id_user + + infos = workspace_util.get_workspace_infos() + assert username in infos + infos = workspace_util.get_workspace_infos(username) + assert username in infos + + workspace_util.delete_workspace(username) + infos = workspace_util.get_workspace_infos() + assert username not in infos + infos = workspace_util.get_workspace_infos(username) + assert username not in infos + + ensure_whole_user(username) + infos = workspace_util.get_workspace_infos() + assert username in infos + infos = workspace_util.get_workspace_infos(username) + assert username in infos + id_user = user_util.get_user_infos(username) + assert not id_user diff --git a/src/layman/error_list.py b/src/layman/error_list.py index 5d86ad6f9..594509c3a 100644 --- a/src/layman/error_list.py +++ b/src/layman/error_list.py @@ -35,12 +35,12 @@ 32: (403, 'Unsuccessful OAuth2 authentication.'), 33: (400, 'Authenticated user did not claim any username within Layman yet.'), 34: (400, 'User already reserved username.'), - 35: (409, 'Username already reserved.'), + 35: (409, 'Workspace name already reserved.'), 36: (409, 'Metadata record already exists.'), 37: (400, 'CSW exception.'), 38: (400, 'Micka HTTP or connection error.'), 39: (404, 'Metadata record does not exists.'), 40: (404, 'Username does not exist.'), 41: (409, 'Username is in conflict with LAYMAN_GS_USER. To resolve this conflict, you can create new GeoServer user with another name to become new LAYMAN_GS_USER, give him LAYMAN_GS_ROLE and ADMIN roles, remove the old LAYMAN_GS_USER user at GeoServer, change environment settings LAYMAN_GS_USER and LAYMAN_GS_PASSWORD, and restart Layman'), - 42: (409, 'Username is in conflict with LAYMAN_PRIME_SCHEMA.'), + 42: (409, 'LAYMAN_PRIME_SCHEMA is in conflict with existing username.'), } diff --git a/src/layman/layer/prime_db_schema/__init__.py b/src/layman/layer/prime_db_schema/__init__.py index 78cdd62b7..c2efbab0e 100644 --- a/src/layman/layer/prime_db_schema/__init__.py +++ b/src/layman/layer/prime_db_schema/__init__.py @@ -1,11 +1,8 @@ -from layman.common.prime_db_schema import users as users_util +from layman.common import prime_db_schema -get_usernames = users_util.get_usernames -check_username = users_util.check_username -ensure_whole_user = users_util.ensure_user -delete_whole_user = users_util.delete_user - - -def check_new_layername(username, layername): - pass +get_usernames = prime_db_schema.get_usernames +check_username = prime_db_schema.check_username +check_new_layername = prime_db_schema.check_new_layername +delete_whole_user = prime_db_schema.delete_whole_user +ensure_whole_user = prime_db_schema.ensure_whole_user diff --git a/src/layman/map/prime_db_schema/__init__.py b/src/layman/map/prime_db_schema/__init__.py index 0ab440406..c2efbab0e 100644 --- a/src/layman/map/prime_db_schema/__init__.py +++ b/src/layman/map/prime_db_schema/__init__.py @@ -1,7 +1,8 @@ -from layman.common.prime_db_schema import users as users_util +from layman.common import prime_db_schema -get_usernames = users_util.get_usernames -check_username = users_util.check_username -ensure_whole_user = users_util.ensure_user -delete_whole_user = users_util.delete_user +get_usernames = prime_db_schema.get_usernames +check_username = prime_db_schema.check_username +check_new_layername = prime_db_schema.check_new_layername +delete_whole_user = prime_db_schema.delete_whole_user +ensure_whole_user = prime_db_schema.ensure_whole_user diff --git a/src/layman/user/rest_users.py b/src/layman/user/rest_users.py new file mode 100644 index 000000000..918b42bdb --- /dev/null +++ b/src/layman/user/rest_users.py @@ -0,0 +1,34 @@ +from flask import Blueprint, jsonify, g + +from layman.common.prime_db_schema import users +from layman.authn import authenticate +from layman.authz import authorize +from layman.util import check_username_decorator +from layman import settings, app + +bp = Blueprint('rest_users', __name__) + + +@bp.before_request +@authenticate +@authorize +def before_request(): + pass + + +@bp.route('', methods=['GET']) +def get(): + app.logger.info(f"GET Users, user={g.user}") + + user_infos = users.get_user_infos() + infos = [ + { + "username": username, + "given_name": info.get("given_name"), + "family_name": info.get("family_name"), + "middle_name": info.get("middle_name"), + "name": info.get("name"), + } + for username, info in user_infos.items() + ] + return jsonify(infos), 200 diff --git a/src/layman/user/rest_users_test.py b/src/layman/user/rest_users_test.py new file mode 100644 index 000000000..e8ea03f65 --- /dev/null +++ b/src/layman/user/rest_users_test.py @@ -0,0 +1,35 @@ +import requests + +from layman import app, settings +from layman.util import url_for +from layman.common.prime_db_schema import ensure_whole_user +from test import process + + +def test_get_users(): + username = 'test_get_users_user' + userinfo = {"iss_id": 'mock_test', + "sub": '1', + "claims": {"email": "test@liferay.com", + "name": "test ensure user", + "given_name": "test", + "family_name": "user", + "middle_name": "ensure", + } + } + + proc = process.start_layman() + + # Create username in layman + with app.app_context(): + ensure_whole_user(username, userinfo) + + # users.GET + url = url_for('rest_users.get') + assert url.endswith('/' + settings.REST_USERS_PREFIX) + + rv = requests.get(url) + assert rv.status_code == 200, rv.json() + assert username in [info["username"] for info in rv.json()] + + process.stop_process(proc) diff --git a/src/layman/user/util.py b/src/layman/user/util.py index d45cc58d8..a6b43e15b 100644 --- a/src/layman/user/util.py +++ b/src/layman/user/util.py @@ -1,8 +1,8 @@ from flask import g, current_app -from layman import LaymanError +from layman import LaymanError, settings from layman.authn import get_open_id_claims, get_iss_id, get_sub from layman.util import slugify, to_safe_names, check_username, get_usernames, ensure_whole_user, delete_whole_user -from layman.authn import redis as authn_redis, filesystem as authn_filesystem +from layman.authn import redis as authn_redis, filesystem as authn_filesystem, prime_db_schema as authn_prime_db_schema def get_user_profile(user_obj): @@ -75,6 +75,7 @@ def _save_reservation(username, claims): sub = get_sub() authn_redis.save_username_reservation(username, iss_id, sub) authn_filesystem.save_username_reservation(username, iss_id, sub, claims) + authn_prime_db_schema.save_username_reservation(username, iss_id, sub, claims) g.user['username'] = username diff --git a/src/layman/util.py b/src/layman/util.py index 9663e4985..ee30d6c31 100644 --- a/src/layman/util.py +++ b/src/layman/util.py @@ -65,9 +65,15 @@ def decorated_function(*args, **kwargs): return decorated_function +def check_reserved_workspace_names(workspace_name): + if workspace_name in settings.RESERVED_WORKSPACE_NAMES: + raise LaymanError(35, {'reserved_by': 'RESERVED_WORKSPACE_NAMES', 'workspace': workspace_name}) + + def check_username(username): if not re.match(USERNAME_RE, username): raise LaymanError(2, {'parameter': 'user', 'expected': USERNAME_RE}) + check_reserved_workspace_names(username) providers = get_internal_providers() call_modules_fn(providers, 'check_username', [username]) diff --git a/src/layman/util_test.py b/src/layman/util_test.py index 34f5ef7e1..d4887ae75 100644 --- a/src/layman/util_test.py +++ b/src/layman/util_test.py @@ -1,3 +1,7 @@ +import pytest + +from . import app as app, LaymanError, settings +from . import util from .util import slugify @@ -8,3 +12,12 @@ def test_slugify(): assert slugify(' ?:"+ @') == '' assert slugify('01 Stanice vodních toků 26.4.2017 (voda)') == \ '01_stanice_vodnich_toku_26_4_2017_voda' + + +def test_check_reserved_workspace_names(): + with app.app_context(): + for username in settings.RESERVED_WORKSPACE_NAMES: + with pytest.raises(LaymanError) as exc_info: + util.check_reserved_workspace_names(username) + assert exc_info.value.code == 35 + assert exc_info.value.data['reserved_by'] == 'RESERVED_WORKSPACE_NAMES' diff --git a/src/layman_settings.py b/src/layman_settings.py index 89848c328..68a93cfe1 100644 --- a/src/layman_settings.py +++ b/src/layman_settings.py @@ -161,4 +161,7 @@ LAYMAN_PUBLIC_URL_SCHEME = urlparse(LAYMAN_CLIENT_PUBLIC_URL).scheme +REST_USERS_PREFIX = 'users' +RESERVED_WORKSPACE_NAMES = {REST_USERS_PREFIX} + # PREFERRED_LANGUAGES = ['cs', 'en'] diff --git a/test/client.py b/test/client.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/process_client.py b/test/process_client.py index 1d402f476..f85cb2a2d 100644 --- a/test/process_client.py +++ b/test/process_client.py @@ -24,8 +24,12 @@ def wait_for_rest(url, max_attempts, sleeping_time, keys_to_check): raise Exception('Max attempts reached!') -def publish_layer(username, layername, file_paths, headers=None): +def publish_layer(username, + layername, + file_paths=None, + headers=None): headers = headers or {} + file_paths = file_paths or ['tmp/naturalearth/110m/cultural/ne_110m_admin_0_countries.geojson'] rest_url = f"http://{settings.LAYMAN_SERVER_NAME}/rest" r_url = f"{rest_url}/{username}/layers"