Skip to content

Commit

Permalink
Move sudo-functionality to sudo.py
Browse files Browse the repository at this point in the history
  • Loading branch information
hmpf committed Nov 10, 2023
1 parent 75a9b60 commit d147a19
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 123 deletions.
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.
"""

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

0 comments on commit d147a19

Please sign in to comment.