mirror of
https://github.com/dkmstr/openuds.git
synced 2025-02-03 13:47:14 +03:00
Added support for skip mfa auth based on groups
This commit is contained in:
parent
cf4a7e0ef9
commit
b9b3889f35
@ -117,7 +117,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
# Extract item db fields
|
||||
# We need this fields for all
|
||||
logger.debug('Saving rule %s / %s', parent, item)
|
||||
fields = self.readFieldsFromParams(
|
||||
fields = self.fields_from_params(
|
||||
[
|
||||
'name',
|
||||
'comments',
|
||||
|
@ -275,7 +275,7 @@ class MetaAssignedService(DetailHandler):
|
||||
if item is None:
|
||||
raise self.invalidItemException()
|
||||
|
||||
fields = self.readFieldsFromParams(['auth_id', 'user_id'])
|
||||
fields = self.fields_from_params(['auth_id', 'user_id'])
|
||||
service = self._getAssignedService(parent, item)
|
||||
user = models.User.objects.get(uuid=process_uuid(fields['user_id']))
|
||||
|
||||
|
@ -154,7 +154,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
# Extract item db fields
|
||||
# We need this fields for all
|
||||
logger.debug('Saving service for %s / %s', parent, item)
|
||||
fields = self.readFieldsFromParams(
|
||||
fields = self.fields_from_params(
|
||||
['name', 'comments', 'data_type', 'tags', 'max_services_count_type']
|
||||
)
|
||||
# Fix max_services_count_type to ServicesCountingType enum or ServicesCountingType.STANDARD if not found
|
||||
|
@ -217,7 +217,7 @@ class AssignedService(DetailHandler):
|
||||
parent = ensure.is_instance(parent, models.ServicePool)
|
||||
if not item:
|
||||
raise self.invalidItemException('Only modify is allowed')
|
||||
fields = self.readFieldsFromParams(['auth_id', 'user_id'])
|
||||
fields = self.fields_from_params(['auth_id', 'user_id'])
|
||||
userService = parent.userServices.get(uuid=process_uuid(item))
|
||||
user = models.User.objects.get(uuid=process_uuid(fields['user_id']))
|
||||
|
||||
|
@ -65,7 +65,7 @@ if typing.TYPE_CHECKING:
|
||||
from uds.models import UserService
|
||||
|
||||
|
||||
def getGroupsFromMeta(groups) -> collections.abc.Iterable[Group]:
|
||||
def get_groups_from_metagroup(groups) -> collections.abc.Iterable[Group]:
|
||||
for g in groups:
|
||||
if g.is_meta:
|
||||
for x in g.groups.all():
|
||||
@ -74,7 +74,7 @@ def getGroupsFromMeta(groups) -> collections.abc.Iterable[Group]:
|
||||
yield g
|
||||
|
||||
|
||||
def getPoolsForGroups(groups):
|
||||
def get_service_pools_for_groups(groups):
|
||||
for servicePool in ServicePool.get_pools_for_groups(groups):
|
||||
yield servicePool
|
||||
|
||||
@ -174,7 +174,7 @@ class Users(DetailHandler):
|
||||
'state': {
|
||||
'title': _('state'),
|
||||
'type': 'dict',
|
||||
'dict': State.dictionary(),
|
||||
'dict': {State.ACTIVE: _('Enabled'), State.INACTIVE: _('Disabled')},
|
||||
}
|
||||
},
|
||||
{'last_access': {'title': _('Last access'), 'type': 'datetime'}},
|
||||
@ -215,7 +215,7 @@ class Users(DetailHandler):
|
||||
valid_fields.append('mfa_data')
|
||||
self._params['mfa_data'] = self._params['mfa_data'].strip()
|
||||
|
||||
fields = self.readFieldsFromParams(valid_fields)
|
||||
fields = self.fields_from_params(valid_fields)
|
||||
if not self._user.is_admin:
|
||||
del fields['staff_member']
|
||||
del fields['is_admin']
|
||||
@ -238,7 +238,7 @@ class Users(DetailHandler):
|
||||
logger.debug('User parent: %s', user.parent)
|
||||
# If internal auth, and not a child user, save groups
|
||||
if not auth.isExternalSource and not user.parent:
|
||||
groups = self.readFieldsFromParams(['groups'])['groups']
|
||||
groups = self.fields_from_params(['groups'])['groups']
|
||||
# Save but skip meta groups, they are not real groups, but just a way to group users based on rules
|
||||
user.groups.set(g for g in parent.groups.filter(uuid__in=groups) if g.is_meta is False)
|
||||
except User.DoesNotExist:
|
||||
@ -296,7 +296,7 @@ class Users(DetailHandler):
|
||||
user = parent.users.get(uuid=process_uuid(uuid))
|
||||
res = []
|
||||
groups = list(user.getGroups())
|
||||
for i in getPoolsForGroups(groups):
|
||||
for i in get_service_pools_for_groups(groups):
|
||||
res.append(
|
||||
{
|
||||
'id': i.uuid,
|
||||
@ -354,6 +354,7 @@ class Groups(DetailHandler):
|
||||
'state': i.state,
|
||||
'type': i.is_meta and 'meta' or 'group',
|
||||
'meta_if_any': i.meta_if_any,
|
||||
'skip_mfa': i.skip_mfa,
|
||||
}
|
||||
if i.is_meta:
|
||||
val['groups'] = list(x.uuid for x in i.groups.all().order_by('name'))
|
||||
@ -364,7 +365,7 @@ class Groups(DetailHandler):
|
||||
raise Exception('Item not found')
|
||||
# Add pools field if 1 item only
|
||||
result = res[0]
|
||||
result['pools'] = [v.uuid for v in getPoolsForGroups([i])]
|
||||
result['pools'] = [v.uuid for v in get_service_pools_for_groups([i])]
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.exception('REST groups')
|
||||
@ -389,9 +390,16 @@ class Groups(DetailHandler):
|
||||
'state': {
|
||||
'title': _('state'),
|
||||
'type': 'dict',
|
||||
'dict': State.dictionary(),
|
||||
'dict': {State.ACTIVE: _('Enabled'), State.INACTIVE: _('Disabled')},
|
||||
}
|
||||
},
|
||||
{
|
||||
'skip_mfa': {
|
||||
'title': _('Skip MFA'),
|
||||
'type': 'dict',
|
||||
'dict': {State.ACTIVE: _('Enabled'), State.INACTIVE: _('Disabled')},
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def get_types(self, parent: 'Model', forType: typing.Optional[str]):
|
||||
@ -428,10 +436,10 @@ class Groups(DetailHandler):
|
||||
logger.debug('Saving group %s / %s', parent, item)
|
||||
logger.debug('Meta any %s', meta_if_any)
|
||||
logger.debug('Pools: %s', pools)
|
||||
valid_fields = ['name', 'comments', 'state']
|
||||
valid_fields = ['name', 'comments', 'state', 'skip_mfa']
|
||||
if self._params.get('name', '') == '':
|
||||
raise RequestError(_('Group name is required'))
|
||||
fields = self.readFieldsFromParams(valid_fields)
|
||||
fields = self.fields_from_params(valid_fields)
|
||||
is_pattern = fields.get('name', '').find('pat:') == 0
|
||||
auth = parent.get_instance()
|
||||
if not item: # Create new
|
||||
@ -455,6 +463,7 @@ class Groups(DetailHandler):
|
||||
del toSave['name'] # Name can't be changed
|
||||
toSave['comments'] = fields['comments'][:255]
|
||||
toSave['meta_if_any'] = meta_if_any
|
||||
toSave['skip_mfa'] = fields['skip_mfa']
|
||||
|
||||
group = parent.groups.get(uuid=process_uuid(item))
|
||||
group.__dict__.update(toSave)
|
||||
@ -496,7 +505,7 @@ class Groups(DetailHandler):
|
||||
uuid = process_uuid(item)
|
||||
group = parent.groups.get(uuid=process_uuid(uuid))
|
||||
res: list[collections.abc.Mapping[str, typing.Any]] = []
|
||||
for i in getPoolsForGroups((group,)):
|
||||
for i in get_service_pools_for_groups((group,)):
|
||||
res.append(
|
||||
{
|
||||
'id': i.uuid,
|
||||
@ -527,7 +536,7 @@ class Groups(DetailHandler):
|
||||
|
||||
if group.is_meta:
|
||||
# Get all users for everygroup and
|
||||
groups = getGroupsFromMeta((group,))
|
||||
groups = get_groups_from_metagroup((group,))
|
||||
tmpSet: typing.Optional[typing.Set] = None
|
||||
for g in groups:
|
||||
gSet = set((i for i in g.users.all()))
|
||||
|
@ -309,7 +309,7 @@ class BaseModelHandler(Handler):
|
||||
'subtitle': subtitle or '',
|
||||
}
|
||||
|
||||
def readFieldsFromParams(self, fldList: list[str]) -> dict[str, typing.Any]:
|
||||
def fields_from_params(self, fldList: list[str]) -> dict[str, typing.Any]:
|
||||
"""
|
||||
Reads the indicated fields from the parameters received, and if
|
||||
:param fldList: List of required fields
|
||||
@ -1095,7 +1095,7 @@ class ModelHandler(BaseModelHandler):
|
||||
|
||||
try:
|
||||
# Extract fields
|
||||
args = self.readFieldsFromParams(self.save_fields)
|
||||
args = self.fields_from_params(self.save_fields)
|
||||
logger.debug('Args: %s', args)
|
||||
self.beforeSave(args)
|
||||
# If tags is in save fields, treat it "specially"
|
||||
|
@ -252,7 +252,7 @@ def register_user(
|
||||
usr.save()
|
||||
if usr is not None and State.is_active(usr.state):
|
||||
# Now we update database groups for this user
|
||||
usr.getManager().recreate_groups(usr)
|
||||
usr.get_manager().recreate_groups(usr)
|
||||
# And add an login event
|
||||
events.add_event(
|
||||
authenticator, events.types.stats.EventType.LOGIN, username=username, srcip=request.ip
|
||||
|
@ -56,7 +56,7 @@ class Group:
|
||||
"""
|
||||
Initializes internal data
|
||||
"""
|
||||
self._manager = db_group.getManager()
|
||||
self._manager = db_group.get_manager()
|
||||
self._db_group = db_group
|
||||
|
||||
def manager(self) -> 'AuthenticatorInstance':
|
||||
|
@ -59,7 +59,7 @@ class User:
|
||||
_groups: typing.Optional[list[Group]]
|
||||
|
||||
def __init__(self, db_user: 'DBUser') -> None:
|
||||
self._manager = db_user.getManager()
|
||||
self._manager = db_user.get_manager()
|
||||
self.grps_manager = None
|
||||
self._db_user = db_user
|
||||
self._groups = None
|
||||
|
@ -310,7 +310,7 @@ class Migration(migrations.Migration):
|
||||
model_name="servicepool",
|
||||
name="short_name",
|
||||
field=models.CharField(default="", max_length=96),
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="metapool",
|
||||
name="name",
|
||||
@ -320,7 +320,7 @@ class Migration(migrations.Migration):
|
||||
model_name="metapool",
|
||||
name="short_name",
|
||||
field=models.CharField(default="", max_length=96),
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="authenticator",
|
||||
name="small_name",
|
||||
@ -346,4 +346,9 @@ class Migration(migrations.Migration):
|
||||
name="unique_id",
|
||||
field=models.CharField(db_index=True, default="", max_length=128),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="group",
|
||||
name="skip_mfa",
|
||||
field=models.CharField(db_index=True, default="I", max_length=1),
|
||||
),
|
||||
]
|
||||
|
@ -69,11 +69,19 @@ class Group(UUIDModel):
|
||||
meta_if_any = models.BooleanField(default=False)
|
||||
groups = models.ManyToManyField('self', symmetrical=False)
|
||||
created = models.DateTimeField(default=sql_datetime, blank=True)
|
||||
skip_mfa = models.CharField(max_length=1, default=State.INACTIVE, db_index=True)
|
||||
|
||||
# "fake" declarations for type checking
|
||||
# objects: 'models.manager.Manager["Group"]'
|
||||
deployedServices: 'models.manager.RelatedManager[ServicePool]'
|
||||
deployedServices: 'models.manager.RelatedManager[ServicePool]' # Legacy name, will keep this forever :)
|
||||
permissions: 'models.manager.RelatedManager[Permissions]'
|
||||
|
||||
@property
|
||||
def service_pools(self) -> 'models.manager.RelatedManager[ServicePool]':
|
||||
"""
|
||||
Returns the service pools that this group has access to
|
||||
"""
|
||||
return self.deployedServices
|
||||
|
||||
class Meta: # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
@ -92,7 +100,7 @@ class Group(UUIDModel):
|
||||
def pretty_name(self) -> str:
|
||||
return self.name + '@' + self.manager.name
|
||||
|
||||
def getManager(self) -> 'auths.Authenticator':
|
||||
def get_manager(self) -> 'auths.Authenticator':
|
||||
"""
|
||||
Returns the authenticator object that owns this user.
|
||||
|
||||
@ -120,7 +128,7 @@ class Group(UUIDModel):
|
||||
|
||||
# We invoke removeGroup. If this raises an exception, group will not
|
||||
# be removed
|
||||
to_delete.getManager().remove_group(to_delete.name)
|
||||
to_delete.get_manager().remove_group(to_delete.name)
|
||||
|
||||
# Clears related logs
|
||||
log.clear_logs(to_delete)
|
||||
|
@ -113,7 +113,7 @@ class User(UUIDModel, properties.PropertiesMixin):
|
||||
The manager (an instance of uds.core.auths.Authenticator), can transform the database stored username
|
||||
so we can, for example, add @domain in some cases.
|
||||
"""
|
||||
return self.getManager().get_for_auth(self.name)
|
||||
return self.get_manager().get_for_auth(self.name)
|
||||
|
||||
@property
|
||||
def pretty_name(self) -> str:
|
||||
@ -121,7 +121,7 @@ class User(UUIDModel, properties.PropertiesMixin):
|
||||
return self.name + '@' + self.manager.name
|
||||
return self.name
|
||||
|
||||
def getManager(self) -> 'auths.Authenticator':
|
||||
def get_manager(self) -> 'auths.Authenticator':
|
||||
"""
|
||||
Returns the authenticator object that owns this user.
|
||||
|
||||
@ -147,7 +147,7 @@ class User(UUIDModel, properties.PropertiesMixin):
|
||||
Invoked to log out this user
|
||||
Returns the url where to redirect user, or None if default url will be used
|
||||
"""
|
||||
return self.getManager().logout(request, self.name)
|
||||
return self.get_manager().logout(request, self.name)
|
||||
|
||||
def getGroups(self) -> typing.Generator['Group', None, None]:
|
||||
"""
|
||||
@ -238,7 +238,7 @@ class User(UUIDModel, properties.PropertiesMixin):
|
||||
|
||||
# first, we invoke removeUser. If this raises an exception, user will not
|
||||
# be removed
|
||||
to_delete.getManager().remove_user(to_delete.name)
|
||||
to_delete.get_manager().remove_user(to_delete.name)
|
||||
|
||||
# If has mfa, remove related data
|
||||
to_delete.cleanRelated()
|
||||
|
File diff suppressed because one or more lines are too long
@ -456,6 +456,9 @@ gettext("Comments");
|
||||
gettext("State");
|
||||
gettext("Enabled");
|
||||
gettext("Disabled");
|
||||
gettext("Skip MFA");
|
||||
gettext("Enabled");
|
||||
gettext("Disabled");
|
||||
gettext("Service Pools");
|
||||
gettext("Match mode");
|
||||
gettext("Any group");
|
||||
|
@ -102,6 +102,6 @@
|
||||
</svg>
|
||||
</div>
|
||||
</uds-root>
|
||||
<script src="/uds/res/admin/runtime.js?stamp=1704668052" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1704668052" type="module"></script><script src="/uds/res/admin/main.js?stamp=1704668052" type="module"></script></body>
|
||||
<script src="/uds/res/admin/runtime.js?stamp=1704725462" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1704725462" type="module"></script><script src="/uds/res/admin/main.js?stamp=1704725462" type="module"></script></body>
|
||||
|
||||
</html>
|
||||
|
@ -43,10 +43,12 @@ from django.http import HttpRequest, HttpResponse, JsonResponse, HttpResponseRed
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from py import log
|
||||
from uds.core.types.request import ExtendedHttpRequest
|
||||
|
||||
from uds.core.types.request import ExtendedHttpRequestWithUser
|
||||
from uds.core.auths import auth
|
||||
from uds.core.util import state
|
||||
from uds.core.util.config import GlobalConfig
|
||||
from uds.core.managers.crypto import CryptoManager
|
||||
from uds.core.managers.user_service import UserServiceManager
|
||||
@ -100,13 +102,13 @@ def ticketLauncher(request: HttpRequest) -> HttpResponse:
|
||||
def login(request: ExtendedHttpRequest, tag: typing.Optional[str] = None) -> HttpResponse:
|
||||
# Default empty form
|
||||
tag = tag or request.session.get('tag', None)
|
||||
|
||||
|
||||
logger.debug('Tag: %s', tag)
|
||||
response: typing.Optional[HttpResponse] = None
|
||||
if request.method == 'POST':
|
||||
request.session['restricted'] = False # Access is from login
|
||||
request.authorized = False # Ensure that on login page, user is unauthorized first
|
||||
|
||||
|
||||
form = LoginForm(request.POST, tag=tag)
|
||||
loginResult = check_login(request, form, tag)
|
||||
if loginResult.user:
|
||||
@ -119,7 +121,11 @@ def login(request: ExtendedHttpRequest, tag: typing.Optional[str] = None) -> Htt
|
||||
|
||||
# If MFA is provided, we need to redirect to MFA page
|
||||
request.authorized = True
|
||||
if loginResult.user.manager.get_type().provides_mfa() and loginResult.user.manager.mfa:
|
||||
if (
|
||||
loginResult.user.manager.get_type().provides_mfa()
|
||||
and loginResult.user.manager.mfa
|
||||
and loginResult.user.groups.filter(skip_mfa=state.State.ACTIVE).count() == 0
|
||||
):
|
||||
request.authorized = False
|
||||
response = HttpResponseRedirect(reverse('page.mfa'))
|
||||
|
||||
@ -152,7 +158,7 @@ def logout(request: ExtendedHttpRequestWithUser) -> HttpResponse:
|
||||
request.authorized = False
|
||||
logoutResponse = request.user.logout(request)
|
||||
url = logoutResponse.url if logoutResponse.success == types.auth.AuthenticationState.REDIRECT else None
|
||||
|
||||
|
||||
return auth.web_logout(request, url or request.session.get('logouturl', None))
|
||||
|
||||
|
||||
@ -169,7 +175,9 @@ def servicesData(request: ExtendedHttpRequestWithUser) -> HttpResponse:
|
||||
|
||||
# The MFA page does not needs CRF token, so we disable it
|
||||
@csrf_exempt
|
||||
def mfa(request: ExtendedHttpRequest) -> HttpResponse: # pylint: disable=too-many-return-statements,too-many-statements
|
||||
def mfa(
|
||||
request: ExtendedHttpRequest,
|
||||
) -> HttpResponse: # pylint: disable=too-many-return-statements,too-many-statements
|
||||
if not request.user or request.authorized: # If no user, or user is already authorized, redirect to index
|
||||
logger.warning('MFA: No user or user is already authorized')
|
||||
return HttpResponseRedirect(reverse('page.index')) # No user, no MFA
|
||||
@ -347,11 +355,12 @@ def update_transport_ticket(
|
||||
uuid=data['ticket-info'].get('userService', None)
|
||||
)
|
||||
UserServiceManager().notify_preconnect(
|
||||
userService, types.connections.ConnectionData(
|
||||
userService,
|
||||
types.connections.ConnectionData(
|
||||
username=username,
|
||||
protocol=data.get('protocol', ''),
|
||||
service_type=data['ticket-info'].get('service_type', ''),
|
||||
)
|
||||
),
|
||||
)
|
||||
except models.UserService.DoesNotExist:
|
||||
pass
|
||||
|
Loading…
x
Reference in New Issue
Block a user