Skip to content

Commit

Permalink
Fix
Browse files Browse the repository at this point in the history
  • Loading branch information
anodos325 committed Dec 13, 2024
1 parent 8ed01b9 commit 5ff86ad
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/middlewared/middlewared/api/base/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def wrapped(*args):
wrapped.audit_callback = audit_callback
wrapped.audit_extended = audit_extended
wrapped.rate_limit = rate_limit
wrapped.roles = roles or []
wrapped.roles = roles or ['FULL_ADMIN']
wrapped._private = private
wrapped._cli_private = cli_private

Expand Down
3 changes: 2 additions & 1 deletion src/middlewared/middlewared/api/v25_04_0/privilege.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from middlewared.api.base import BaseModel, Excluded, excluded_field, ForUpdateMetaclass, NonEmptyString, SID
from middlewared.utils.security import STIGType
from .group import GroupEntry

__all__ = ["PrivilegeEntry", "PrivilegeRoleEntry",
Expand Down Expand Up @@ -58,4 +59,4 @@ class PrivilegeRoleEntry(BaseModel):
title: NonEmptyString
includes: list[NonEmptyString]
builtin: bool
gpos_stig: bool
stig: STIGType | None
7 changes: 7 additions & 0 deletions src/middlewared/middlewared/plugins/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,13 @@ async def common_validation(self, verrors, data, schema, group_ids, old=None):
f'{schema}.password_disabled', 'Password authentication may not be disabled for SMB users.'
)

if combined['smb'] and (await self.middleware.call('system.security.config'))['enable_gpos_stig']:
verrors.add(
f'{schema}.smb',
'SMB authentication for local user accounts is not permitted when General Purpose OS '
'STIG compatibility is enabled.'
)

password = data.get('password')
if not old and not password and not data.get('password_disabled'):
verrors.add(f'{schema}.password', 'Password is required')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async def roles(self, filters, options):
"title": name,
"includes": role.includes,
"builtin": role.builtin,
"gpos_stig": role.stig,
"stig": role.stig,
}
for name, role in ROLES.items()
]
Expand Down
2 changes: 2 additions & 0 deletions src/middlewared/middlewared/plugins/security/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ async def do_update(self, job, data):
)
await self.configure_security_on_ha(is_ha, job, RebootReason.GPOSSTIG)

await self.configure_stig(new)

return await self.config()


Expand Down
2 changes: 1 addition & 1 deletion src/middlewared/middlewared/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Role:
'SYSTEM_AUDIT_READ': Role(),
'SYSTEM_AUDIT_WRITE': Role(),

'FULL_ADMIN': Role(full_admin=True, builtin=False, stig=None),
'FULL_ADMIN': Role(full_admin=True, builtin=False),

# Alert roles
'ALERT_LIST_READ': Role(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def product_type(product_type='SCALE_ENTERPRISE'):


@contextlib.contextmanager
def enable_stig():
def set_stig_available():
with product_type():
with mock('system.security.config', return_value={'id': 1, 'enable_fips': True, 'enable_gpos_stig': True}):
with set_fips_available():
yield
118 changes: 82 additions & 36 deletions tests/api2/test_stig.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import pytest

from middlewared.service_exception import CallError
from middlewared.test.integration.assets.product import enable_stig, product_type, set_fips_available
from middlewared.service_exception import ValidationErrors as Verr
from middlewared.test.integration.assets.account import user
from middlewared.test.integration.assets.product import product_type, set_fips_available
from middlewared.test.integration.assets.two_factor_auth import (
enabled_twofactor_auth, get_user_secret, get_2fa_totp_token
)
from middlewared.test.integration.utils import call, client
from middlewared.test.integration.utils import call, client, ssh
from truenas_api_client import ValidationErrors


Expand All @@ -24,46 +26,28 @@ def community_product():
yield


@pytest.fixture(scope='function')
@pytest.fixture(scope='module')
def two_factor_enabled():
with enabled_twofactor_auth() as two_factor_config:
yield two_factor_config


@pytest.fixture(scope='function')
def setup_stig():
# We need authenticated client to undo assurance level
with client() as c:
with enable_stig():
# Force reconfiguration for STIG
call('system.security.configure_stig', {'enable_gpos_stig': True})
aal = call('auth.get_authenticator_assurance_level')
assert aal == 'LEVEL_2'

try:
yield
finally:
# Drop assurance level so that we can remove mock
# reliably
call('system.security.configure_stig', {'enable_gpos_stig': False})
aal = call('auth.get_authenticator_assurance_level')
assert aal == 'LEVEL_1'


@pytest.fixture(scope='function')
@pytest.fixture(scope='module')
def two_factor_non_admin(two_factor_enabled, unprivileged_user_fixture):
privilege = call('privilege.query', [['local_groups.0.group', '=', unprivileged_user_fixture.group_name]])
assert len(privilege) > 0, 'Privilege not found'
call('privilege.update', privilege[0]['id'], {'roles': ['SHARING_ADMIN']})

try:
call('user.renew_2fa_secret', unprivileged_user_fixture.username, {'interval': 60})
yield unprivileged_user_fixture
user_obj_id = call('user.query', [['username', '=', unprivileged_user_fixture.username]], {'get': True})['id']
secret = get_user_secret(user_obj_id)
yield (unprivileged_user_fixture, secret)
finally:
call('privilege.update', privilege[0]['id'], {'roles': []})


@pytest.fixture(scope='function')
@pytest.fixture(scope='module')
def two_factor_full_admin(two_factor_enabled, unprivileged_user_fixture):
privilege = call('privilege.query', [['local_groups.0.group', '=', unprivileged_user_fixture.group_name]])
assert len(privilege) > 0, 'Privilege not found'
Expand Down Expand Up @@ -95,6 +79,39 @@ def do_stig_auth(c, user_obj, secret):
assert resp['response_type'] == 'SUCCESS'


@pytest.fixture(scope='module')
def setup_stig(two_factor_full_admin):
""" Configure STIG and yield admin user object and an authenticated session """
user_obj, secret = two_factor_full_admin

# Create websocket connection from prior to STIG being enabled to pass to
# test methods. This connection will have unrestricted privileges (due to
# privilege_compose happening before STIG).
#
# Tests validating what can be performed under STIG restrictions should create
# a new websocket session
with product_type('SCALE_ENTERPRISE'):
with set_fips_available(True):
with client(auth=None) as c:
# Do two-factor authentication before enabling STIG support
do_stig_auth(c, user_obj, secret)
c.call('system.security.update', {'enable_fips': True, 'enable_gpos_stig': True}, job=True)
aal = c.call('auth.get_authenticator_assurance_level')
assert aal == 'LEVEL_2'

try:
yield {
'connection': c,
'user_obj': user_obj,
'secret': secret
}
finally:
c.call('system.security.update', {'enable_fips': False, 'enable_gpos_stig': False}, job=True)


# The order of the following tests is significant. We gradully fixtures that have module scope
# as we finish checking for correct ValidationErrors

def test_nonenterprise_fail(community_product):
with pytest.raises(ValidationErrors, match='Please contact iX sales for more information.'):
call('system.security.update', {'enable_gpos_stig': True}, job=True)
Expand Down Expand Up @@ -126,10 +143,12 @@ def test_no_current_cred_no_2fa(enterprise_product, two_factor_full_admin):
call('system.security.update', {'enable_fips': True, 'enable_gpos_stig': True}, job=True)


def test_stig_enabled_authenticator_assurance_level(setup_stig, two_factor_full_admin):
# At this point STIG should be enabled on TrueNAS until end of file


def test_stig_enabled_authenticator_assurance_level(setup_stig):
# Validate that admin user can authenticate and perform operations
with client(auth=None) as c:
do_stig_auth(c, *two_factor_full_admin)
setup_stig['connection'].call('system.info')

# Auth for account without 2fa should fail
with pytest.raises(CallError) as ce:
Expand All @@ -138,17 +157,44 @@ def test_stig_enabled_authenticator_assurance_level(setup_stig, two_factor_full_

assert ce.value.errno == errno.EOPNOTSUPP

# We should also be able to create a new websocket connection
# The previous one was created before enabling STIG
with client(auth=None) as c:
do_stig_auth(c, setup_stig['user_obj'], setup_stig['secret'])
c.call('system.info')


def test_stig_roles_decrease(setup_stig, two_factor_full_admin):
def test_stig_roles_decrease(setup_stig):

# We need new webosocket connection to verify that privileges
# are appropriately decreased
with client(auth=None) as c:
do_stig_auth(c, *two_factor_full_admin)
do_stig_auth(c, setup_stig['user_obj'], setup_stig['secret'])

me = c.call('auth.me')
assert 'FULL_ADMIN' not in me['privilege']['roles']
for role in c.call('privilege.roles'):
if role['gpos_stig']:
assert role in me['privilege']['roles']
if role['stig'] is not None:
assert role['name'] in me['privilege']['roles']
else:
assert role not in me['privilege']['roles']
assert role['name'] not in me['privilege']['roles']

assert me['privilege']['web_shell'] is False
assert me['privilege']['webui_access'] is True


def test_stig_smb_auth_disabled(setup_stig):
# We need new webosocket connection to verify that privileges
# are appropriately decreased

smb_user_cnt = setup_stig['connection'].call('user.query', [['smb', '=', True]], {'count': True})
assert smb_user_cnt == 0

assert me['webshell'] is False
# We shouldn't be able to create new SMB users
with pytest.raises(Verr, match='General Purpose OS STIG'):
setup_stig['connection'].call('user.create', {
'username': 'CANARY',
'full_name': 'CANARY',
'password': 'CANARY',
'group_create': True,
'smb': True
})
10 changes: 8 additions & 2 deletions tests/unit/test_role_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
@pytest.fixture(scope='module')
def nostig_roles():
# Generate list of expected roles that should be unavailble for STIG mode
PREFIXES = ('VM', 'TRUECOMMAND', 'CATALOG', 'DOCKER', 'APPS', 'VIRT', 'NOSTIG_PREFIX')
PREFIXES = ('VM', 'TRUECOMMAND', 'CATALOG', 'DOCKER', 'APPS', 'VIRT')
yield set([
role_name for
role_name in list(ROLES.keys()) if role_name.startswith(PREFIXES) and not role_name.endswith('READ')
]) | set(['FULL_ADMIN'])
])


@pytest.fixture(scope='module')
Expand All @@ -50,6 +50,12 @@ def test__roles_have_correct_stig_assignment(nostig_roles, role_name):
else:
assert role_name in nostig_roles

# There should only be one role that grants full admin privileges
if role_name == 'FULL_ADMIN':
assert role_to_check.full_admin is True
else:
assert role_to_check.full_admin is False


@pytest.mark.parametrize('role,method,enabled_stig_type,resources', [
('READONLY_ADMIN', 'CALL', None, READONLY_ADMIN_METHODS),
Expand Down

0 comments on commit 5ff86ad

Please sign in to comment.