Compare commits

..

8 Commits

Author SHA1 Message Date
Elijah DeLee
19e3cba35c Merge branch 'devel' into x-request-id 2024-04-24 15:04:13 -05:00
irozet12
e4646ae611 Add help message for expiration tokens (#15076) (#15077)
Co-authored-by: Ирина Розет <irozet@astralinux.ru>
2024-04-24 19:58:09 +00:00
Elijah DeLee
c11ff49a56 fixup syntax 2024-04-24 14:48:02 -05:00
Bruno Sanchez
7dc77546f4 Adding CSRF Validation for schemas (#15027)
* Adding CSRF Validation for schemas

* Changing retrieve of scheme to avoid importing new library

* check if CSRF_TRUSTED_ORIGINS exists before accessing it

---------

Signed-off-by: Bruno Sanchez <brsanche@redhat.com>
2024-04-24 15:47:03 -04:00
Michael Tipton
f5f85666c8 Add ability to set SameSite policy for userLoggedIn cookie (#15100)
* Add ability to set SameSite policy for userLoggedIn cookie

* reformat line for linter
2024-04-24 15:44:31 -04:00
Alan Rominger
47a061eb39 Fix and test data migration error from DAB RBAC (#15138)
* Fix and test data migration error from DAB RBAC

* Fix up migration test

* Fix custom method bug

* Fix another fat fingered bug
2024-04-24 15:14:03 -04:00
Elijah DeLee
51bcf82cf4 include x-request-id header in perf log if exists 2024-04-24 13:51:42 -05:00
Alan Rominger
c760577855 Adjust test for stricter DAB user view permission enforcement (#15130) 2024-04-23 15:21:06 -04:00
10 changed files with 84 additions and 7 deletions

View File

@@ -95,7 +95,9 @@ class LoggedLoginView(auth_views.LoginView):
ret = super(LoggedLoginView, self).post(request, *args, **kwargs) ret = super(LoggedLoginView, self).post(request, *args, **kwargs)
if request.user.is_authenticated: if request.user.is_authenticated:
logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None)))) logger.info(smart_str(u"User {} logged in from {}".format(self.request.user.username, request.META.get('REMOTE_ADDR', None))))
ret.set_cookie('userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False)) ret.set_cookie(
'userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False), samesite=getattr(settings, 'USER_COOKIE_SAMESITE', 'Lax')
)
ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
return ret return ret

View File

@@ -2,6 +2,7 @@
import logging import logging
# Django # Django
from django.core.checks import Error
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# Django REST Framework # Django REST Framework
@@ -954,3 +955,27 @@ def logging_validate(serializer, attrs):
register_validate('logging', logging_validate) register_validate('logging', logging_validate)
def csrf_trusted_origins_validate(serializer, attrs):
if not serializer.instance or not hasattr(serializer.instance, 'CSRF_TRUSTED_ORIGINS'):
return attrs
if 'CSRF_TRUSTED_ORIGINS' not in attrs:
return attrs
errors = []
for origin in attrs['CSRF_TRUSTED_ORIGINS']:
if "://" not in origin:
errors.append(
Error(
"As of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS "
"setting must start with a scheme (usually http:// or "
"https://) but found %s. See the release notes for details." % origin,
)
)
if errors:
error_messages = [error.msg for error in errors]
raise serializers.ValidationError(_('\n'.join(error_messages)))
return attrs
register_validate('system', csrf_trusted_origins_validate)

View File

@@ -58,7 +58,7 @@ class TimingMiddleware(threading.local, MiddlewareMixin):
response['X-API-Profile-File'] = self.prof.stop() response['X-API-Profile-File'] = self.prof.stop()
perf_logger.debug( perf_logger.debug(
f'request: {request}, response_time: {response["X-API-Total-Time"]}', f'request: {request}, response_time: {response["X-API-Total-Time"]}',
extra=dict(python_objects=dict(request=request, response=response, X_API_TOTAL_TIME=response["X-API-Total-Time"])), extra=dict(python_objects=dict(request=request, response=response, X_API_TOTAL_TIME=response["X-API-Total-Time"], x_request_id=request.get('x-request-id', 'not-set'))),
) )
return response return response

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('main', '0189_inbound_hop_nodes'), ('main', '0189_inbound_hop_nodes'),
] ]

View File

@@ -140,6 +140,17 @@ def get_permissions_for_role(role_field, children_map, apps):
return perm_list return perm_list
def model_class(ct, apps):
"""
You can not use model methods in migrations, so this duplicates
what ContentType.model_class does, using current apps
"""
try:
return apps.get_model(ct.app_label, ct.model)
except LookupError:
return None
def migrate_to_new_rbac(apps, schema_editor): def migrate_to_new_rbac(apps, schema_editor):
""" """
This method moves the assigned permissions from the old rbac.py models This method moves the assigned permissions from the old rbac.py models
@@ -197,7 +208,7 @@ def migrate_to_new_rbac(apps, schema_editor):
role_definition = managed_definitions[permissions] role_definition = managed_definitions[permissions]
else: else:
action = role.role_field.rsplit('_', 1)[0] # remove the _field ending of the name action = role.role_field.rsplit('_', 1)[0] # remove the _field ending of the name
role_definition_name = f'{role.content_type.model_class().__name__} {action.title()}' role_definition_name = f'{model_class(role.content_type, apps).__name__} {action.title()}'
description = role_descriptions[role.role_field] description = role_descriptions[role.role_field]
if type(description) == dict: if type(description) == dict:

View File

@@ -40,10 +40,25 @@ def test_custom_system_roles_prohibited(admin_user, post):
@pytest.mark.django_db @pytest.mark.django_db
def test_assign_managed_role(admin_user, alice, rando, inventory, post, managed_roles): def test_assignment_to_invisible_user(admin_user, alice, rando, inventory, post, managed_roles):
"Alice can not see rando, and so can not give them a role assignment"
rd = RoleDefinition.objects.get(name='Inventory Admin') rd = RoleDefinition.objects.get(name='Inventory Admin')
rd.give_permission(alice, inventory) rd.give_permission(alice, inventory)
# Now that alice has full permissions to the inventory, she will give rando permission url = django_reverse('roleuserassignment-list')
r = post(url=url, data={"user": rando.id, "role_definition": rd.id, "object_id": inventory.id}, user=alice, expect=400)
assert 'does not exist' in str(r.data)
assert not rando.has_obj_perm(inventory, 'change')
@pytest.mark.django_db
def test_assign_managed_role(admin_user, alice, rando, inventory, post, managed_roles, organization):
rd = RoleDefinition.objects.get(name='Inventory Admin')
rd.give_permission(alice, inventory)
# When alice and rando are members of the same org, they can see each other
member_rd = RoleDefinition.objects.get(name='Organization Member')
for u in (alice, rando):
member_rd.give_permission(u, organization)
# Now that alice has full permissions to the inventory, and can see rando, she will give rando permission
url = django_reverse('roleuserassignment-list') url = django_reverse('roleuserassignment-list')
post(url=url, data={"user": rando.id, "role_definition": rd.id, "object_id": inventory.id}, user=alice, expect=201) post(url=url, data={"user": rando.id, "role_definition": rd.id, "object_id": inventory.id}, user=alice, expect=201)
assert rando.has_obj_perm(inventory, 'change') is True assert rando.has_obj_perm(inventory, 'change') is True

View File

@@ -1,6 +1,7 @@
import pytest import pytest
from django_test_migrations.plan import all_migrations, nodes_to_tuples from django_test_migrations.plan import all_migrations, nodes_to_tuples
from django.utils.timezone import now
""" """
Most tests that live in here can probably be deleted at some point. They are mainly Most tests that live in here can probably be deleted at some point. They are mainly
@@ -68,3 +69,19 @@ class TestMigrationSmoke:
bar_peers = bar.peers.all() bar_peers = bar.peers.all()
assert len(bar_peers) == 1 assert len(bar_peers) == 1
assert fooaddr in bar_peers assert fooaddr in bar_peers
def test_migrate_DAB_RBAC(self, migrator):
old_state = migrator.apply_initial_migration(('main', '0190_alter_inventorysource_source_and_more'))
Organization = old_state.apps.get_model('main', 'Organization')
User = old_state.apps.get_model('auth', 'User')
org = Organization.objects.create(name='arbitrary-org', created=now(), modified=now())
user = User.objects.create(username='random-user')
org.read_role.members.add(user)
new_state = migrator.apply_tested_migration(
('main', '0192_custom_roles'),
)
RoleUserAssignment = new_state.apps.get_model('dab_rbac', 'RoleUserAssignment')
assert RoleUserAssignment.objects.filter(user=user.id, object_id=org.id).exists()

View File

@@ -277,6 +277,9 @@ SESSION_COOKIE_SECURE = True
# Note: This setting may be overridden by database settings. # Note: This setting may be overridden by database settings.
SESSION_COOKIE_AGE = 1800 SESSION_COOKIE_AGE = 1800
# Option to change userLoggedIn cookie SameSite policy.
USER_COOKIE_SAMESITE = 'Lax'
# Name of the cookie that contains the session information. # Name of the cookie that contains the session information.
# Note: Changing this value may require changes to any clients. # Note: Changing this value may require changes to any clients.
SESSION_COOKIE_NAME = 'awx_sessionid' SESSION_COOKIE_NAME = 'awx_sessionid'

View File

@@ -38,7 +38,9 @@ class CompleteView(BaseRedirectView):
response = super(CompleteView, self).dispatch(request, *args, **kwargs) response = super(CompleteView, self).dispatch(request, *args, **kwargs)
if self.request.user and self.request.user.is_authenticated: if self.request.user and self.request.user.is_authenticated:
logger.info(smart_str(u"User {} logged in".format(self.request.user.username))) logger.info(smart_str(u"User {} logged in".format(self.request.user.username)))
response.set_cookie('userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False)) response.set_cookie(
'userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False), samesite=getattr(settings, 'USER_COOKIE_SAMESITE', 'Lax')
)
response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid'))
return response return response

View File

@@ -78,12 +78,14 @@ function MiscAuthenticationEdit() {
default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS, default: OAUTH2_PROVIDER_OPTIONS.default.ACCESS_TOKEN_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type, type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Access Token Expiration`, label: t`Access Token Expiration`,
help_text: t`Access Token Expiration in seconds`,
}, },
REFRESH_TOKEN_EXPIRE_SECONDS: { REFRESH_TOKEN_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS, ...OAUTH2_PROVIDER_OPTIONS,
default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS, default: OAUTH2_PROVIDER_OPTIONS.default.REFRESH_TOKEN_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type, type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Refresh Token Expiration`, label: t`Refresh Token Expiration`,
help_text: t`Refresh Token Expiration in seconds`,
}, },
AUTHORIZATION_CODE_EXPIRE_SECONDS: { AUTHORIZATION_CODE_EXPIRE_SECONDS: {
...OAUTH2_PROVIDER_OPTIONS, ...OAUTH2_PROVIDER_OPTIONS,
@@ -91,6 +93,7 @@ function MiscAuthenticationEdit() {
OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS, OAUTH2_PROVIDER_OPTIONS.default.AUTHORIZATION_CODE_EXPIRE_SECONDS,
type: OAUTH2_PROVIDER_OPTIONS.child.type, type: OAUTH2_PROVIDER_OPTIONS.child.type,
label: t`Authorization Code Expiration`, label: t`Authorization Code Expiration`,
help_text: t`Authorization Code Expiration in seconds`,
}, },
}; };