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

165 Apply roles for Get Workspace Layer #965

Merged
merged 9 commits into from
Dec 5, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
### Migrations and checks
#### Schema migrations
- [#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.
- [#164](https://github.com/LayerManager/layman/issues/165) Create internal GeoServer [JDBC Role Service](https://docs.geoserver.org/2.21.x/en/user/security/usergrouprole/roleservices.html#jdbc-role-service) DB schema `_role_service`.
#### 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 respect role access rights:
- [GET](doc/rest.md#get-workspace-layer) Workspace Layer
- [#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
Expand Down
4 changes: 4 additions & 0 deletions src/layman/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@
from .layer.prime_db_schema.wfs_wms_status import set_after_restart
set_after_restart()

logger.info(f'Recreate Role Service admin role views')
from .authz.role_service import ensure_admin_roles
ensure_admin_roles()

pipe.multi()
pipe.set(LAYMAN_DEPS_ADJUSTED_KEY, 'done')
pipe.execute()
Expand Down
14 changes: 12 additions & 2 deletions src/layman/authz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from layman import LaymanError, settings, authn, util as layman_util, common
from layman.common.prime_db_schema import workspaces, users
from layman.common.rest import parse_request_path
from . import role_service


def authorize(workspace, publication_type, publication_name, request_method, actor_name):
Expand Down Expand Up @@ -72,8 +73,11 @@ def authorize_after_multi_get_request(actor_name, response):


def is_user_in_access_rule(username, access_rule_names):
return settings.RIGHTS_EVERYONE_ROLE in access_rule_names \
or (username and username in access_rule_names)
usernames, rolenames = split_user_and_role_names(access_rule_names)
userroles = role_service.get_user_roles(username)
return settings.RIGHTS_EVERYONE_ROLE in rolenames \
or (username and username in usernames) \
or (set(rolenames).intersection(userroles))


def can_user_publish_in_public_workspace(username):
Expand Down Expand Up @@ -149,3 +153,9 @@ def complete_access_rights(access_rights_to_complete, full_access_rights):
if right_type not in access_rights_to_complete:
access_rights_to_complete[right_type] = full_access_rights[right_type]
return access_rights_to_complete


def split_user_and_role_names(user_and_role_names):
user_names = [name for name in user_and_role_names if any(letter.islower() for letter in name)]
role_names = [name for name in user_and_role_names if name not in user_names]
return user_names, role_names
15 changes: 14 additions & 1 deletion src/layman/authz/authz_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from layman import app, settings, LaymanError
from test_tools import process_client
from test_tools.util import url_for
from . import authorize_workspace_publications_decorator
from . import authorize_workspace_publications_decorator, split_user_and_role_names


@authorize_workspace_publications_decorator
Expand Down Expand Up @@ -165,3 +165,16 @@ def test_authorize_publications_decorator_on_rest_api(
self.assert_response(response, authz_status_code, authz_response)
response = requests.get(rest_url, timeout=settings.DEFAULT_CONNECTION_TIMEOUT)
self.assert_response(response, authz_status_code, authz_response)


@pytest.mark.parametrize('roles_and_users, exp_users, exp_roles', [
pytest.param([], [], [], id='no-names'),
pytest.param(['user1', 'user2'], ['user1', 'user2'], [], id='only-users'),
pytest.param(['ROLE1', 'EVERYONE'], [], ['ROLE1', 'EVERYONE'], id='only-roles'),
pytest.param(['ROLE2', 'user1', 'EVERYONE', 'user2'], ['user1', 'user2'], ['ROLE2', 'EVERYONE'],
id='more-users-and-roles'),
])
def test_split_user_and_role_names(roles_and_users, exp_users, exp_roles):
user_names, role_names = split_user_and_role_names(roles_and_users)
assert user_names == exp_users
assert role_names == exp_roles
37 changes: 37 additions & 0 deletions src/layman/authz/role_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from db import util as db_util
from layman import settings

ROLE_NAME_PATTERN = r'^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$'
ROLE_SERVICE_SCHEMA = settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA


def ensure_admin_roles():
create_admin_roles_view = f"""CREATE OR REPLACE view {ROLE_SERVICE_SCHEMA}.admin_roles
as
select 'ADMIN' as name
UNION ALL
select 'GROUP_ADMIN'
UNION ALL
select %s
;"""
db_util.run_statement(create_admin_roles_view, (settings.LAYMAN_GS_ROLE, ))

create_admin_user_roles_view = f"""CREATE OR REPLACE view {ROLE_SERVICE_SCHEMA}.admin_user_roles
as
select %s as username, %s as rolename
index-git marked this conversation as resolved.
Show resolved Hide resolved
UNION ALL
select %s, 'ADMIN'
;"""
db_util.run_statement(create_admin_user_roles_view, (settings.LAYMAN_GS_USER, settings.LAYMAN_GS_ROLE, settings.LAYMAN_GS_USER))


def get_user_roles(username):
index-git marked this conversation as resolved.
Show resolved Hide resolved
query = f"""
select rolename from {ROLE_SERVICE_SCHEMA}.user_roles
where username = %s
and rolename not in (%s, %s, %s)
and LEFT(rolename, 5) != 'USER_'
and rolename ~ %s
"""
roles = db_util.run_query(query, (username, 'ADMIN', 'GROUP_ADMIN', settings.LAYMAN_GS_ROLE, ROLE_NAME_PATTERN))
return {role[0] for role in roles}
13 changes: 0 additions & 13 deletions src/layman/common/prime_db_schema/publications_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,16 +524,3 @@ def test_validation(self, username, pre_publication_update_info, publication_upd
publication_update_info,
)
assert exc_info.value.code == 43


@pytest.mark.parametrize('roles_and_users, exp_users, exp_roles', [
pytest.param([], [], [], id='no-names'),
pytest.param(['user1', 'user2'], ['user1', 'user2'], [], id='only-users'),
pytest.param(['ROLE1', 'EVERYONE'], [], ['ROLE1', 'EVERYONE'], id='only-roles'),
pytest.param(['ROLE2', 'user1', 'EVERYONE', 'user2'], ['user1', 'user2'], ['ROLE2', 'EVERYONE'],
id='more-users-and-roles'),
])
def test_split_user_and_role_names(roles_and_users, exp_users, exp_roles):
user_names, role_names = publications.split_user_and_role_names(roles_and_users)
assert user_names == exp_users
assert role_names == exp_roles
1 change: 1 addition & 0 deletions src/layman/upgrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
]),
((1, 23, 0), [
upgrade_v1_23.adjust_db_for_roles,
upgrade_v1_23.create_role_service_schema,
]),
],
consts.MIGRATION_TYPE_DATA: [
Expand Down
78 changes: 78 additions & 0 deletions src/layman/upgrade/upgrade_v1_23.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from db import util as db_util
from layman import settings
from layman.authz import role_service as role_service_util

logger = logging.getLogger(__name__)
DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA
ROLE_SERVICE_SCHEMA = settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA


def adjust_db_for_roles():
Expand All @@ -24,3 +26,79 @@ def adjust_db_for_roles():
'''

db_util.run_statement(statement)


def create_role_service_schema():
logger.info(f' Create internal 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)

create_role_table = f"""create table {ROLE_SERVICE_SCHEMA}.bussiness_roles(
id integer GENERATED ALWAYS AS IDENTITY,
name varchar(64) not null,
parent varchar(64) references {ROLE_SERVICE_SCHEMA}.bussiness_roles (name),
CONSTRAINT bussiness_roles_pkey PRIMARY KEY (id),
CONSTRAINT bussiness_roles_name_key UNIQUE (name)
);"""
db_util.run_statement(create_role_table)

create_role_table = f"""create table {ROLE_SERVICE_SCHEMA}.bussiness_user_roles(
id integer GENERATED ALWAYS AS IDENTITY,
username varchar(128) not null references {DB_SCHEMA}.workspaces (name),
rolename varchar(64) not null references {ROLE_SERVICE_SCHEMA}.bussiness_roles (name),
CONSTRAINT bussiness_user_roles_pkey PRIMARY KEY (id),
CONSTRAINT bussiness_user_roles_username_rolename_key UNIQUE (username,rolename)
);"""
db_util.run_statement(create_role_table)

create_layman_users_roles_view = f"""create view {ROLE_SERVICE_SCHEMA}.layman_users_roles
as
select concat('USER_', UPPER(w.name)) as name
from {DB_SCHEMA}.users u inner join
{DB_SCHEMA}.workspaces w on w.id = u.id_workspace
;"""
db_util.run_statement(create_layman_users_roles_view)

create_layman_users_user_roles_view = f"""create view {ROLE_SERVICE_SCHEMA}.layman_users_user_roles
as
select w.name as username,
concat('USER_', UPPER(w.name)) as rolename
from {DB_SCHEMA}.users u inner join
{DB_SCHEMA}.workspaces w on w.id = u.id_workspace
;"""
db_util.run_statement(create_layman_users_user_roles_view)

role_service_util.ensure_admin_roles()

create_roles_view = f"""create view {ROLE_SERVICE_SCHEMA}.roles
as
select name,
parent
from {ROLE_SERVICE_SCHEMA}.bussiness_roles
UNION ALL
select name,
null
from {ROLE_SERVICE_SCHEMA}.layman_users_roles
UNION ALL
select name,
null
from {ROLE_SERVICE_SCHEMA}.admin_roles
;"""
db_util.run_statement(create_roles_view)

create_user_roles_view = f"""create view {ROLE_SERVICE_SCHEMA}.user_roles
as
select username,
rolename
from {ROLE_SERVICE_SCHEMA}.bussiness_user_roles
UNION ALL
select username,
rolename
from {ROLE_SERVICE_SCHEMA}.layman_users_user_roles
UNION ALL
select username,
rolename
from {ROLE_SERVICE_SCHEMA}.admin_user_roles
;"""
db_util.run_statement(create_user_roles_view)
70 changes: 70 additions & 0 deletions src/layman/upgrade/upgrade_v1_23_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from db import util as db_util
from layman import app, settings
from layman.authz import role_service as role_service_util
from layman.common.prime_db_schema import ensure_whole_user
from test_tools import process_client
from . import upgrade_v1_23

DB_SCHEMA = settings.LAYMAN_PRIME_SCHEMA
ROLE_SERVICE_SCHEMA = settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA


@pytest.mark.usefixtures('ensure_layman', 'oauth2_provider_mock')
Expand Down Expand Up @@ -58,3 +61,70 @@ def test_adjust_db_for_roles():
assert len(rights_rows) == 1
assert rights_rows[0][1] is not None, f"id_user is none!"
assert rights_rows[0][2] is None, f"role_name is not none!"


def test_create_role_service_schema():
username = 'test_create_role_service_schema_username'
rolename = f'USER_{username.upper()}'
userinfo = {"issuer_id": 'mock_test_users_test',
"sub": '10',
"claims": {"email": "[email protected]",
"name": "test ensure user",
"preferred_username": 'test_preferred',
"given_name": "test",
"family_name": "user",
"middle_name": "ensure",
}
}
drop_statement = f'''DROP SCHEMA IF EXISTS {ROLE_SERVICE_SCHEMA} CASCADE;'''
schema_existence_query = f'''SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '{ROLE_SERVICE_SCHEMA}';'''
table_existence_query = f'''SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '{ROLE_SERVICE_SCHEMA}' and table_name = %s;'''
layman_users_roles_query = f'''select COUNT(*) from {ROLE_SERVICE_SCHEMA}.layman_users_roles where name = %s'''
layman_users_user_roles_query = f'''select COUNT(*) from {ROLE_SERVICE_SCHEMA}.layman_users_user_roles where username = %s and rolename = %s'''
admin_roles_query = f'''select COUNT(*) from {ROLE_SERVICE_SCHEMA}.admin_roles'''
admin_user_roles_query = f'''select COUNT(*) from {ROLE_SERVICE_SCHEMA}.admin_user_roles where username = %s and rolename = %s'''
roles_query = f'''select
(select count(*) from {ROLE_SERVICE_SCHEMA}.bussiness_roles) bussiness_roles,
(select count(*) from {ROLE_SERVICE_SCHEMA}.layman_users_roles) layman_users_roles,
(select count(*) from {ROLE_SERVICE_SCHEMA}.admin_roles) admin_roles,
(select count(*) from {ROLE_SERVICE_SCHEMA}.roles) roles'''
user_roles_query = f'''select
(select count(*) from {ROLE_SERVICE_SCHEMA}.bussiness_user_roles) bussiness_user_roles,
(select count(*) from {ROLE_SERVICE_SCHEMA}.layman_users_user_roles) layman_users_user_roles,
(select count(*) from {ROLE_SERVICE_SCHEMA}.admin_user_roles) admin_user_roles,
(select count(*) from {ROLE_SERVICE_SCHEMA}.user_roles) user_roles'''

with app.app_context():
ensure_whole_user(username, userinfo)
db_util.run_statement(drop_statement)
result = db_util.run_query(schema_existence_query)[0][0]
assert result == 0

upgrade_v1_23.create_role_service_schema()

result = db_util.run_query(schema_existence_query)[0][0]
assert result == 1
result = db_util.run_query(table_existence_query, ('bussiness_roles',))[0][0]
assert result == 1
result = db_util.run_query(table_existence_query, ('bussiness_user_roles',))[0][0]
assert result == 1
result = db_util.run_query(layman_users_roles_query, (rolename,))[0][0]
assert result == 1
result = db_util.run_query(layman_users_user_roles_query, (username, rolename,))[0][0]
assert result == 1
result = db_util.run_query(admin_roles_query)[0][0]
assert result == 3
result = db_util.run_query(admin_user_roles_query, ('layman_test', 'LAYMAN_TEST_ROLE',))[0][0]
assert result == 1
result = db_util.run_query(admin_user_roles_query, ('layman_test', 'ADMIN',))[0][0]
assert result == 1
result = db_util.run_query(roles_query)[0]
assert result[0] + result[1] + result[2] == result[3]
result = db_util.run_query(user_roles_query)[0]
assert result[0] + result[1] + result[2] == result[3]

role_service_util.ensure_admin_roles()
result = db_util.run_query(roles_query)[0]
assert result[0] + result[1] + result[2] == result[3]
result = db_util.run_query(user_roles_query)[0]
assert result[0] + result[1] + result[2] == result[3]
5 changes: 4 additions & 1 deletion src/layman_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ class EnumWfsWmsStatus(Enum):
LAYMAN_PRIME_SCHEMA = os.environ['LAYMAN_PRIME_SCHEMA']
assert re.match("[a-z_][a-z0-9_]*", LAYMAN_PRIME_SCHEMA), "Only lowercase characters, numbers and underscore " \
"should be used for " \
"PG_PRIME_SCHEMA. "
"LAYMAN_PRIME_SCHEMA. "

# Name of schema, where Layman maintains internal GS JDBC Role Service.
LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA = '_role_service'

# List of schemas that are not allowed to be used as usernames.
PG_NON_USER_SCHEMAS = [
Expand Down
2 changes: 2 additions & 0 deletions test_tools/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ def oauth2_provider_mock():
'test_adjust_db_for_roles_ws': None,
'test_adjust_db_for_roles_ws2': None,
'test_access_rights_role_user1': None,
'test_role_application_user': None,
'test_role_application_role_user': None,
},
},
'host': '0.0.0.0',
Expand Down
23 changes: 23 additions & 0 deletions test_tools/role_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from db import util as db_util
from layman import settings


def ensure_role(rolename):
insert_role_statement = f'''insert into {settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA}.bussiness_roles(name) values (%s) ON CONFLICT (name) DO nothing;'''
db_util.run_statement(insert_role_statement, (rolename,))


def delete_role(rolename):
delete_statement = f"""delete from {settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA}.bussiness_roles where name = %s;"""
db_util.run_statement(delete_statement, (rolename,))


def ensure_user_role(username, rolename):
ensure_role(rolename)
insert_user_role_statement = f'''insert into {settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA}.bussiness_user_roles(username, rolename) values (%s, %s) ON CONFLICT (username, rolename) DO nothing;'''
db_util.run_statement(insert_user_role_statement, (username, rolename,))


def delete_user_role(username, rolename):
delete_statement = f"""delete from {settings.LAYMAN_INTERNAL_ROLE_SERVICE_SCHEMA}.bussiness_user_roles where username = %s and rolename = %s;"""
db_util.run_statement(delete_statement, (username, rolename,))
Loading
Loading