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

Refactor web auth #2706

Merged
merged 8 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion python/nav/django/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from django.conf import settings

from nav.config import find_config_file
from nav.web.auth import get_sudoer, get_login_url, get_logout_url
from nav.web.auth import get_login_url, get_logout_url
from nav.web.auth.sudo import get_sudoer
from nav.django.utils import get_account, is_admin
from nav.web.message import Messages
from nav.web.webfront.utils import tool_list, quick_read, split_tools
Expand Down
115 changes: 3 additions & 112 deletions python/nav/web/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,15 @@


from nav.auditlog.models import LogEntry
from nav.django.utils import is_admin, get_account
from nav.models.profiles import Account, AccountGroup
from nav.web.auth import ldap, remote_user
from nav.web.auth.sudo import desudo, get_sudoer
from nav.web.auth.utils import _set_account, ACCOUNT_ID_VAR


_logger = logging.getLogger(__name__)


ACCOUNT_ID_VAR = 'account_id'
SUDOER_ID_VAR = 'sudoer'

# This may seem like redundant information, but it seems django's reverse
# will hang under some usages of these middleware classes - so until we figure
# out what's going on, we'll hardcode this here.
Expand All @@ -57,13 +55,6 @@
LOGOUT_URL = '/index/logout/'


def _set_account(request, account):
request.session[ACCOUNT_ID_VAR] = account.id
request.account = account
_logger.debug('Set active account to "%s"', account.login)
request.session.save()


def authenticate(username, password):
"""Authenticate username and password against database.
Returns account object if user was authenticated, else None.
Expand Down Expand Up @@ -184,7 +175,7 @@ def process_request(self, request):
)
if logged_in.id == Account.DEFAULT_ACCOUNT:
# Switch from anonymous to REMOTE_USER
login_remote_user(request)
remote_user.login(request)
elif remote_username != logged_in.login:
# REMOTE_USER has changed, logout
logout(request, sudo=bool(sudo_operator))
Expand Down Expand Up @@ -220,23 +211,6 @@ def ensure_account(request):
_set_account(request, account)


def login_remote_user(request):
"""Log in the user in REMOTE_USER, if any and enabled

:return: Account for remote user, or None
:rtype: Account, None
"""
remote_username = remote_user.get_username(request)
if remote_username:
# Get or create an account from the REMOTE_USER http header
account = remote_user.authenticate(request)
if account:
request.session[ACCOUNT_ID_VAR] = account.id
request.account = account
return account
return None


class AuthorizationMiddleware(MiddlewareMixin):
def process_request(self, request):
account = request.account
Expand Down Expand Up @@ -310,89 +284,6 @@ def logout(request, sudo=False):
return u'/'


#
# sudo-related functionality
#


def sudo(request, other_user):
"""Switches the current session to become other_user"""
if SUDOER_ID_VAR in request.session:
# Already logged in as another user.
raise SudoRecursionError()
if not is_admin(get_account(request)):
# Check if sudoer is acctually admin
raise SudoNotAdminError()
original_user = request.account
request.session[SUDOER_ID_VAR] = original_user.id
_set_account(request, other_user)
_logger.info('Sudo: "%s" acting as "%s"', original_user, other_user)
_logger.debug(
'Sudo: (session: %s, account: %s)', dict(request.session), request.account
)
LogEntry.add_log_entry(
original_user,
'sudo',
'{actor} sudoed to {target}',
subsystem='auth',
target=other_user,
)


def desudo(request):
"""Switches the current session to become the original user from before a
call to sudo().

"""
if SUDOER_ID_VAR not in request.session:
# We are not sudoing
return

other_user = request.account
original_user_id = request.session[SUDOER_ID_VAR]
original_user = Account.objects.get(id=original_user_id)

del request.session[ACCOUNT_ID_VAR]
del request.session[SUDOER_ID_VAR]
_set_account(request, original_user)
_logger.info(
'DeSudo: "%s" no longer acting as "%s"', original_user, request.account
)
_logger.debug(
'DeSudo: (session: %s, account: %s)', dict(request.session), request.account
)
LogEntry.add_log_entry(
original_user,
'desudo',
'{actor} no longer sudoed as {target}',
subsystem='auth',
target=other_user,
)


def get_sudoer(request):
"""Returns a sudoer's Account, if current session is in sudo-mode"""
if SUDOER_ID_VAR in request.session:
return Account.objects.get(id=request.session[SUDOER_ID_VAR])


class SudoRecursionError(Exception):
msg = u"Already posing as another user"

def __str__(self):
return self.msg


class SudoNotAdminError(Exception):
msg = u"Not admin"

def __str__(self):
return self.msg


# For testing


def create_session_cookie(username):
"""Creates an active session for username and returns the resulting
session cookie.
Expand Down
18 changes: 18 additions & 0 deletions python/nav/web/auth/remote_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from nav.auditlog.models import LogEntry
from nav.config import NAVConfigParser
from nav.models.profiles import Account
from nav.web.auth.utils import ACCOUNT_ID_VAR

try:
# Python 3.6+
Expand Down Expand Up @@ -98,6 +99,23 @@ def authenticate(request):
return account


def login(request):
"""Log in the user in REMOTE_USER, if any and enabled

:return: Account for remote user, or None
:rtype: Account, None
"""
remote_username = get_username(request)
if remote_username:
# Get or create an account from the REMOTE_USER http header
account = authenticate(request)
if account:
request.session[ACCOUNT_ID_VAR] = account.id
request.account = account
return account
return None


def get_loginurl(request):
"""Return a url (if set) to log in to/via a remote service

Expand Down
108 changes: 108 additions & 0 deletions python/nav/web/auth/sudo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright (C) 2010, 2011, 2013, 2019 Uninett AS
# Copyright (C) 2022 Sikt
#
# This file is part of Network Administration Visualized (NAV).
#
# NAV is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License version 3 as published by
# the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details. You should have received a copy of the GNU General Public
# License along with NAV. If not, see <http://www.gnu.org/licenses/>.
#
"""
Contains web authentication and login functionality for NAV.

The "*Middleware" is Django-specific.
"""
hmpf marked this conversation as resolved.
Show resolved Hide resolved

import logging

from nav.auditlog.models import LogEntry
from nav.django.utils import is_admin, get_account
from nav.models.profiles import Account
from nav.web.auth.utils import _set_account, ACCOUNT_ID_VAR


_logger = logging.getLogger(__name__)


SUDOER_ID_VAR = 'sudoer'


def sudo(request, other_user):
"""Switches the current session to become other_user"""
if SUDOER_ID_VAR in request.session:
# Already logged in as another user.
raise SudoRecursionError()
if not is_admin(get_account(request)):
# Check if sudoer is acctually admin
raise SudoNotAdminError()
original_user = request.account
request.session[SUDOER_ID_VAR] = original_user.id
_set_account(request, other_user)
_logger.info('Sudo: "%s" acting as "%s"', original_user, other_user)
_logger.debug(
'Sudo: (session: %s, account: %s)', dict(request.session), request.account
)
LogEntry.add_log_entry(
original_user,
'sudo',
'{actor} sudoed to {target}',
subsystem='auth',
target=other_user,
)


def desudo(request):
"""Switches the current session to become the original user from before a
call to sudo().

"""
if SUDOER_ID_VAR not in request.session:
# We are not sudoing
return

other_user = request.account
original_user_id = request.session[SUDOER_ID_VAR]
original_user = Account.objects.get(id=original_user_id)

del request.session[ACCOUNT_ID_VAR]
del request.session[SUDOER_ID_VAR]
_set_account(request, original_user)
_logger.info(
'DeSudo: "%s" no longer acting as "%s"', original_user, request.account
)
_logger.debug(
'DeSudo: (session: %s, account: %s)', dict(request.session), request.account
)
LogEntry.add_log_entry(
original_user,
'desudo',
'{actor} no longer sudoed as {target}',
subsystem='auth',
target=other_user,
)


def get_sudoer(request):
"""Returns a sudoer's Account, if current session is in sudo-mode"""
if SUDOER_ID_VAR in request.session:
return Account.objects.get(id=request.session[SUDOER_ID_VAR])


class SudoRecursionError(Exception):
msg = u"Already posing as another user"

def __str__(self):
return self.msg


class SudoNotAdminError(Exception):
msg = u"Not admin"

def __str__(self):
return self.msg
30 changes: 30 additions & 0 deletions python/nav/web/auth/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright (C) 2010, 2011, 2013, 2019 Uninett AS
# Copyright (C) 2022 Sikt
#
# This file is part of Network Administration Visualized (NAV).
#
# NAV is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License version 3 as published by
# the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details. You should have received a copy of the GNU General Public
# License along with NAV. If not, see <http://www.gnu.org/licenses/>.
#

import logging


_logger = logging.getLogger(__name__)


ACCOUNT_ID_VAR = 'account_id'


def _set_account(request, account):
request.session[ACCOUNT_ID_VAR] = account.id
request.account = account
_logger.debug('Set active account to "%s"', account.login)
request.session.save()
2 changes: 1 addition & 1 deletion python/nav/web/navlets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

from nav.models.profiles import AccountNavlet, AccountDashboard
from nav.models.manage import Sensor
from nav.web.auth import get_sudoer
from nav.web.auth.sudo import get_sudoer
from nav.django.utils import get_account
from nav.web.utils import require_param
from nav.web.webfront import find_dashboard
Expand Down
2 changes: 1 addition & 1 deletion python/nav/web/webfront/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from nav.auditlog.models import LogEntry
from nav.django.utils import get_account
from nav.models.profiles import NavbarLink, AccountDashboard, AccountNavlet
from nav.web.auth import ACCOUNT_ID_VAR
from nav.web.auth.utils import ACCOUNT_ID_VAR
from nav.web.auth import logout as auth_logout
from nav.web import auth
from nav.web.auth import ldap
Expand Down
12 changes: 8 additions & 4 deletions tests/unittests/general/web_middleware_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from django.test import RequestFactory

from nav.web.auth import ACCOUNT_ID_VAR
from nav.web.auth import SUDOER_ID_VAR
from nav.web.auth.utils import ACCOUNT_ID_VAR
from nav.web.auth.sudo import SUDOER_ID_VAR
from nav.web.auth import AuthenticationMiddleware
from nav.web.auth import AuthorizationMiddleware
from nav.web.auth import logout
Expand Down Expand Up @@ -159,7 +159,9 @@ def test_process_request_switch_users(self):
side_effect=auth._set_account(fake_request, ANOTHER_PLAIN_ACCOUNT),
):
with patch('nav.web.auth.logout'):
AuthenticationMiddleware(lambda x: x).process_request(fake_request)
AuthenticationMiddleware(lambda x: x).process_request(
fake_request
)
assert fake_request.account == ANOTHER_PLAIN_ACCOUNT
assert (
ACCOUNT_ID_VAR in fake_request.session
Expand Down Expand Up @@ -199,7 +201,9 @@ def test_process_request_not_authorized(self):
'nav.web.auth.AuthorizationMiddleware.redirect_to_login',
return_value='here',
):
result = AuthorizationMiddleware(lambda x: x).process_request(fake_request)
result = AuthorizationMiddleware(lambda x: x).process_request(
fake_request
)
assert result == 'here'
assert os.environ.get('REMOTE_USER', None) != PLAIN_ACCOUNT.login

Expand Down
Loading