Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On POST/PATCH Workspace Layer/Map insert/update role name to DB #964

Merged
merged 17 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
- [#165](https://github.com/LayerManager/layman/issues/165) Add column `role_name` to table `rights` in prime DB schema. Add constraint that exactly one of columns `role_name` and `id_user` is not null.
#### Data migrations
### Changes
- [#165](https://github.com/LayerManager/layman/issues/165) POST Workspace [Layers](doc/rest.md#post-workspace-layers)/[Maps](doc/rest.md#post-workspace-maps) and PATCH Workspace [Layer](doc/rest.md#patch-workspace-layer)/[Map](doc/rest.md#patch-workspace-map) saves [role names](doc/models.md#role) mentioned in `access_rights.read` and `access_rights.write` parameters into DB.
- [#165](https://github.com/LayerManager/layman/issues/165) Many endpoints return previously associated [role names](doc/models.md#role) in `access_rights.read` and `access_rights.write` keys:
- [GET](doc/rest.md#get-workspace-layer)/[PATCH](doc/rest.md#patch-workspace-layer) Workspace Layer
- [GET](doc/rest.md#get-workspace-map)/[PATCH](doc/rest.md#patch-workspace-map) Workspace Map
- GET Workspace [Layers](doc/rest.md#get-workspace-layers)/[Maps](doc/rest.md#get-workspace-maps)
- GET [Layers](doc/rest.md#get-layers)/[Maps](doc/rest.md#get-maps)/[Publications](doc/rest.md#get-publications)
- All changes from [v1.22.1](#v1221) and [v1.22.2](#v1222).
- [#960](https://github.com/LayerManager/layman/issues/960) Handle WMS requests with HTTP error more efficiently in timgen.
- [#962](https://github.com/LayerManager/layman/issues/962) Make values of `layman_metadata.publication_status` and `status` key(s) more consistent in responses of PATCH Workspace [Layer](doc/rest.md#patch-workspace-layer)/[Map](doc/rest.md#patch-workspace-map) and GET Workspace [Layer](doc/rest.md#get-workspace-layer)/[Map](doc/rest.md#get-workspace-map).
Expand Down
2 changes: 1 addition & 1 deletion doc/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Layman supports two main models of geospatial data: layers and maps. **Layer** i
There are multiple client applications for communication with Layman through its REST API: simple web test client shipped with Layman, QGIS desktop client, and HSLayers library. Published data are accessible through standardized OGC APIs: Web Map Service, Web Feature Service, and Catalogue Service.

### Security
Layman`s security system uses two well-known concepts: authentication and authorization. Common configuration consists of authentication based on widely used OAuth2 protocol and authorization allows users to give read and write access rights to each user on publication level.
Layman`s security system uses two well-known concepts: authentication and authorization. Common configuration consists of authentication based on widely used OAuth2 protocol and authorization allows users to give read and write access rights to users and roles on publication level.

### Scalability
Large data files can be easily published thanks to chunk upload. Asynchronous processing ensures fast communication with REST API. Processing tasks can be distributed on multiple servers. Layman also stands on the shoulders of widely used programs like Flask, PostgreSQL, PostGIS, GDAL, GeoServer, QGIS Server, Celery, and Redis.
95 changes: 62 additions & 33 deletions src/layman/common/prime_db_schema/publications.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,23 @@ def get_publication_infos_with_metainfo(workspace_name=None, pub_type=None, styl
p.srid as srid,
PGP_SYM_DECRYPT(p.external_table_uri, p.uuid::text)::json external_table_uri,
(select rtrim(concat(case when u.id is not null then w.name || ',' end,
string_agg(w2.name, ',') || ',',
index-git marked this conversation as resolved.
Show resolved Hide resolved
string_agg(COALESCE(w2.name, r.role_name), ',' ORDER BY COALESCE(w2.name, r.role_name)) || ',',
case when p.everyone_can_read then %s || ',' end
), ',')
from {DB_SCHEMA}.rights r inner join
{DB_SCHEMA}.users u2 on r.id_user = u2.id inner join
from {DB_SCHEMA}.rights r left join
{DB_SCHEMA}.users u2 on r.id_user = u2.id left join
{DB_SCHEMA}.workspaces w2 on w2.id = u2.id_workspace
where r.id_publication = p.id
and r.type = 'read') can_read_users,
and r.type = 'read') read_users_roles,
(select rtrim(concat(case when u.id is not null then w.name || ',' end,
string_agg(w2.name, ',') || ',',
index-git marked this conversation as resolved.
Show resolved Hide resolved
string_agg(COALESCE(w2.name, r.role_name), ',' ORDER BY COALESCE(w2.name, r.role_name)) || ',',
case when p.everyone_can_write then %s || ',' end
), ',')
from {DB_SCHEMA}.rights r inner join
{DB_SCHEMA}.users u2 on r.id_user = u2.id inner join
from {DB_SCHEMA}.rights r left join
{DB_SCHEMA}.users u2 on r.id_user = u2.id left join
{DB_SCHEMA}.workspaces w2 on w2.id = u2.id_workspace
where r.id_publication = p.id
and r.type = 'write') can_write_users,
and r.type = 'write') write_users_roles,
(select json_agg(json_build_object(
'name', ml.layer_name,
'workspace', ml.layer_workspace,
Expand Down Expand Up @@ -271,14 +271,14 @@ def get_publication_infos_with_metainfo(workspace_name=None, pub_type=None, styl
'original_data_source': settings.EnumOriginalDataSource.TABLE.value if external_table_uri else settings.EnumOriginalDataSource.FILE.value,
'native_bounding_box': [xmin, ymin, xmax, ymax],
'native_crs': db_util.get_crs_from_srid(srid, use_internal_srid=True) if srid else None,
'access_rights': {'read': can_read_users.split(','),
'write': can_write_users.split(',')},
'access_rights': {'read': read_users_roles.split(','),
'write': write_users_roles.split(',')},
'_map_layers': map_layers or [],
'_layer_maps': layer_maps or [],
'_wfs_wms_status': settings.EnumWfsWmsStatus(wfs_wms_status) if wfs_wms_status else None,
}
for id_publication, workspace_name, publication_type, publication_name, title, uuid, geodata_type, style_type, image_mosaic, updated_at, xmin, ymin, xmax, ymax,
srid, external_table_uri, can_read_users, can_write_users, map_layers, layer_maps, wfs_wms_status, _
srid, external_table_uri, read_users_roles, write_users_roles, map_layers, layer_maps, wfs_wms_status, _
in values}

infos = {key: {**value,
Expand Down Expand Up @@ -339,17 +339,21 @@ def secure_bbox_transform(bbox_crs):
return bbox_sql


def only_valid_names(users_list):
def only_valid_user_names(users_list):
usernames_for_check = set(users_list)
usernames_for_check.discard(ROLE_EVERYONE)
for username in usernames_for_check:
info = users.get_user_infos(username)
if not info:
raise LaymanError(43, f'Not existing user. Username={username}')


def at_least_one_can_write(can_write):
if not can_write:
# pylint: disable=unused-argument
def only_valid_role_names(roles_list):
pass


def at_least_one_can_write(user_names, role_names):
if not user_names and ROLE_EVERYONE not in role_names:
raise LaymanError(43, f'At least one user have to have write rights.')


Expand All @@ -370,18 +374,32 @@ def owner_can_still_write(owner,
raise LaymanError(43, f'Owner of the personal workspace have to keep write right.')


def is_user(user_or_role_name):
return any(letter.islower() for letter in user_or_role_name)


def split_user_and_role_names(user_and_role_names):
user_names = [name for name in user_and_role_names if is_user(name)]
role_names = [name for name in user_and_role_names if name not in user_names]
return user_names, role_names


def check_rights_axioms(can_read,
can_write,
actor_name,
owner,
can_read_old=None,
can_write_old=None):
if can_read:
only_valid_names(can_read)
read_users, read_roles = split_user_and_role_names(can_read)
only_valid_user_names(read_users)
only_valid_role_names(read_roles)
if can_write:
only_valid_names(can_write)
write_users, write_roles = split_user_and_role_names(can_write)
only_valid_user_names(write_users)
only_valid_role_names(write_roles)
owner_can_still_write(owner, can_write)
at_least_one_can_write(can_write)
at_least_one_can_write(write_users, write_roles)
i_can_still_write(actor_name, can_write)
if can_read or can_write:
can_read_check = can_read or can_read_old
Expand Down Expand Up @@ -412,13 +430,18 @@ def check_publication_info(workspace_name, info):
}) from exc_info


def clear_roles(users_list, workspace_name):
result_set = set(users_list)
result_set.discard(ROLE_EVERYONE)
def get_user_and_role_names_for_db(users_and_roles_list, workspace_name):
user_names, role_names = split_user_and_role_names(users_and_roles_list)

users_set = set(user_names)
user_info = users.get_user_infos(workspace_name)
if user_info:
result_set.discard(workspace_name)
return result_set
users_set.discard(workspace_name)

roles_set = set(role_names)
roles_set.discard(ROLE_EVERYONE)

return users_set, roles_set


def insert_publication(workspace_name, info):
Expand Down Expand Up @@ -455,13 +478,15 @@ def insert_publication(workspace_name, info):
)
pub_id = db_util.run_query(insert_publications_sql, data)[0][0]

read_users = clear_roles(info['access_rights']['read'], workspace_name)
write_users = clear_roles(info['access_rights']['write'], workspace_name)
read_users, read_roles = get_user_and_role_names_for_db(info['access_rights']['read'], workspace_name)
write_users, write_roles = get_user_and_role_names_for_db(info['access_rights']['write'], workspace_name)
rights.insert_rights(pub_id,
read_users,
read_roles,
'read')
rights.insert_rights(pub_id,
write_users,
write_roles,
'write')
return pub_id

Expand All @@ -474,8 +499,10 @@ def update_publication(workspace_name, info):
for right_type in right_type_list:
access_rights_changes[right_type] = {
'EVERYONE': None,
'add': set(),
'remove': set(),
'add_users': set(),
'add_roles': set(),
'remove_users': set(),
'remove_roles': set(),
}

external_table_uri = psycopg2.extras.Json({
Expand All @@ -500,10 +527,12 @@ def update_publication(workspace_name, info):
if info['access_rights'].get(right_type):
usernames_list = info["access_rights"].get(right_type)
access_rights_changes[right_type]['EVERYONE'] = ROLE_EVERYONE in usernames_list
usernames_list_clear = clear_roles(usernames_list, workspace_name)
usernames_old_list_clear = clear_roles(access_rights_changes[right_type]['username_list_old'], workspace_name)
access_rights_changes[right_type]['add'] = usernames_list_clear.difference(usernames_old_list_clear)
access_rights_changes[right_type]['remove'] = usernames_old_list_clear.difference(usernames_list_clear)
usernames_list_clear, roles_list_clear = get_user_and_role_names_for_db(usernames_list, workspace_name)
usernames_old_list_clear, roles_old_list_clear = get_user_and_role_names_for_db(access_rights_changes[right_type]['username_list_old'], workspace_name)
access_rights_changes[right_type]['add_users'] = usernames_list_clear.difference(usernames_old_list_clear)
access_rights_changes[right_type]['add_roles'] = roles_list_clear.difference(roles_old_list_clear)
access_rights_changes[right_type]['remove_users'] = usernames_old_list_clear.difference(usernames_list_clear)
access_rights_changes[right_type]['remove_roles'] = roles_old_list_clear.difference(roles_list_clear)

update_publications_sql = f'''update {DB_SCHEMA}.publications set
title = coalesce(%s, title),
Expand Down Expand Up @@ -534,8 +563,8 @@ def update_publication(workspace_name, info):
pub_id = db_util.run_query(update_publications_sql, data)[0][0]

for right_type in right_type_list:
rights.insert_rights(pub_id, access_rights_changes[right_type]['add'], right_type)
rights.remove_rights(pub_id, access_rights_changes[right_type]['remove'], right_type)
rights.insert_rights(pub_id, access_rights_changes[right_type]['add_users'], access_rights_changes[right_type]['add_roles'], right_type)
rights.remove_rights(pub_id, access_rights_changes[right_type]['remove_users'], access_rights_changes[right_type]['remove_roles'], right_type)

return pub_id

Expand Down
Loading
Loading