CVE-2024-29199 (Python, published 2024-03-26T03:15Z)
Description: value: Nautobot is a Network Source of Truth and Network Automation Platform. A number of Nautobot URL endpoints were found to be improperly accessible to unauthenticated (anonymous) users. These endpoints will not disclose any Nautobot data to an unauthenticated user unless the Nautobot configuration variable EXEMPT_VIEW_PERMISSIONS is changed from its default value (an empty list) to permit access to specific data by unauthenticated users. This vulnerability is fixed in 1.6.16 and 2.1.9.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/nautobot/nautobot
Files changed:
changes/5464.added
changes/5464.changed
changes/5464.documentation
changes/5464.fixed
changes/5464.housekeeping
changes/5464.removed
changes/5464.security
examples/example_plugin/example_plugin/api/urls.py
examples/example_plugin/example_plugin/views.py
examples/example_plugin_with_view_override/example_plugin_with_view_override/views.py
nautobot/apps/api.py
nautobot/apps/utils.py
nautobot/apps/views.py
nautobot/circuits/api/urls.py
nautobot/circuits/api/views.py
nautobot/core/api/routers.py
nautobot/core/api/utils.py
nautobot/core/api/views.py
nautobot/core/settings.py
nautobot/core/tests/integration/test_view_authentication.py
nautobot/core/tests/test_graphql.py
nautobot/core/tests/test_views.py
nautobot/core/utils/lookup.py
nautobot/core/views/__init__.py
nautobot/core/views/generic.py
nautobot/dcim/api/urls.py
nautobot/dcim/api/views.py
nautobot/dcim/tests/test_api.py
nautobot/dcim/views.py
nautobot/docs/development/apps/api/views/core-view-overrides.md
nautobot/docs/development/apps/api/views/django-generic-views.md
nautobot/extras/api/urls.py
nautobot/extras/api/views.py
nautobot/extras/plugins/views.py
nautobot/extras/tests/test_views.py
nautobot/extras/views.py
nautobot/ipam/api/urls.py
nautobot/ipam/api/views.py
nautobot/ipam/tests/test_graphql.py
nautobot/ipam/views.py
nautobot/tenancy/api/urls.py
nautobot/tenancy/api/views.py
nautobot/users/api/urls.py
nautobot/users/api/views.py
nautobot/users/views.py
nautobot/virtualization/api/urls.py
nautobot/virtualization/api/views.py
Commit message: View authentication and permission fixes (#5464)



Vulnerable code:
router = OrderedDefaultRouter()
from django.views.generic import View
class ExamplePluginHomeView(View):
class ExamplePluginConfigView(View):
class ViewToBeOverridden(View):
from django.views import generic
class ViewOverride(generic.View):
from nautobot.core.api.routers import OrderedDefaultRouter
    APIRootView,
router = OrderedDefaultRouter()
router.APIRootView = views.CircuitsRootView
from rest_framework.routers import APIRootView


class CircuitsRootView(APIRootView):
    """
    Circuits API root view
    """

    def get_view_name(self):
        return "Circuits"


    def __init__(self, *args, **kwargs):
from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated
class APIRootView(NautobotAPIVersionMixin, APIView):
    This is the root of the REST API. API endpoints are arranged by app and model name; e.g. `/api/dcim/locations/`.
    _ignore_model_permissions = True
    def get_view_name(self):
        return "API Root"
        if not request.user.is_authenticated:
            doc_url = reverse("api_docs")
            login_url = reverse(settings.LOGIN_URL)
            return redirect(f"{login_url}?next={doc_url}")

    The code is a stripped down version of graphene-django default View
    https://github.com/graphql-python/graphene-django/blob/main/graphene_django/views.py#L57
    permission_classes = [AllowAny]
        """Validate unauthenticated users are not able to query anything by default."""
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertIsInstance(response.data["data"]["racks"], list)
        names = [item["name"] for item in response.data["data"]["racks"]]
        self.assertEqual(names, [])

    @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
    def test_graphql_api_no_token_exempt(self):
        """Validate unauthenticated users are able to query based on the exempt permissions."""
        response = self.client.post(self.api_url, {"query": self.get_racks_query}, format="json")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertIsInstance(response.data["data"]["racks"], list)
        names = [item["name"] for item in response.data["data"]["racks"]]
        self.assertEqual(names, ["Rack 1-1", "Rack 1-2", "Rack 2-1", "Rack 2-2"])
    @override_settings(BANNER_TOP="Hello, Banner Top", BANNER_BOTTOM="Hello, Banner Bottom")
    def test_routes_redirect_back_to_login_unauthenticated(self):
        """Assert that api docs and graphql redirects to login page if user is unauthenticated."""
        urls = [reverse("api_docs"), reverse("graphql")]
            response = self.client.get(url, follow=True, **headers)
            self.assertHttpStatus(response, 200)
            redirect_chain = [(f"/login/?next={url}", 302)]
            self.assertEqual(response.redirect_chain, redirect_chain)
            response_content = response.content.decode(response.charset).replace("\n", "")
            # Assert Footer items(`self.footer_elements`), Banner and Banner Top is hidden
            for footer_text in self.footer_elements:
                self.assertNotIn(footer_text, response_content)
            # Only API Docs implements BANNERS
            if url == urls[0]:
                self.assertNotIn("Hello, Banner Top", response_content)
                self.assertNotIn("Hello, Banner Bottom", response_content)
from django.shortcuts import get_object_or_404, redirect, render
class StaticMediaFailureView(View):
class CustomGraphQLView(GraphQLView):
        if not request.user.is_authenticated:
            graphql_url = reverse("graphql")
            login_url = reverse(settings.LOGIN_URL)
            return redirect(f"{login_url}?next={graphql_url}")
router = OrderedDefaultRouter()
router.APIRootView = views.DCIMRootView
from rest_framework.routers import APIRootView

class DCIMRootView(APIRootView):
    """
    DCIM API root view
    """

    def get_view_name(self):
        return "DCIM"


            Interface.objects.all(),
from django.shortcuts import get_object_or_404, redirect, render
        }[kwargs.get("termination_b_type")]
class DeviceViewOverride(generic.View):
from django.views.generic import View
class RandomAnimalView(View):
router = OrderedDefaultRouter()
router.APIRootView = views.ExtrasRootView
from rest_framework.routers import APIRootView
class ExtrasRootView(APIRootView):
    """
    Extras API root view
    """

    def get_view_name(self):
        return "Extras"


from django.contrib.auth.mixins import LoginRequiredMixin
from nautobot.core.api.views import NautobotAPIVersionMixin
class InstalledPluginDetailView(LoginRequiredMixin, View):
    _ignore_model_permissions = True
class PluginsAPIRootView(NautobotAPIVersionMixin, APIView):
    _ignore_model_permissions = True

    def get_view_name(self):
        return "Plugins"
class ObjectDynamicGroupsView(View):
        repository = get_object_or_404(GitRepository, pk=pk)
class GitRepositorySyncView(View):
class GitRepositoryDryRunView(View):
class JobLogEntryTableView(View):
        instance = self.queryset.get(pk=pk)
class ObjectChangeLogView(View):
class ObjectNotesView(View):
        notes_table = tables.NoteTable(obj.notes)
class SecretProviderParametersFormView(View):
router = OrderedDefaultRouter()
router.APIRootView = views.IPAMRootView
from rest_framework.routers import APIRootView

class IPAMRootView(APIRootView):
    """
    IPAM API root view
    """

    def get_view_name(self):
        return "IPAM"


from django.test import override_settings
    @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
        response = self.client.post(self.api_url, payload, format="json")
                return redirect(self.get_return_url(request), default_return_url="ipam:ipaddress_add")
    default_return_url = "ipam:ipaddress_add"
        # Redirect user if an interface has not been provided
        if "interface" not in request.GET and "vminterface" not in request.GET:
            return redirect(self.get_return_url(request))
        _, error_msg = retrieve_interface_or_vminterface_from_request(request)
        if error_msg:
            messages.warning(request, error_msg)
            return redirect(self.get_return_url(request))
router = OrderedDefaultRouter()
router.APIRootView = views.TenancyRootView
from rest_framework.routers import APIRootView


class TenancyRootView(APIRootView):
    """
    Tenancy API root view
    """

    def get_view_name(self):
        return "Tenancy"


router = OrderedDefaultRouter()
router.APIRootView = views.UsersRootView
from django.contrib.auth import get_user_model, login, logout
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.routers import APIRootView

class UsersRootView(APIRootView):
    """
    Users API root view
    """

    def get_view_name(self):
        return "Users"


    @action(methods=["GET"], detail=False, url_path="my-profile")
    def my_profile(self, request):
        serializer = self.serializer_class(instance=request.user, context={"request": request})
        return Response(serializer.data)

    @method_decorator(ensure_csrf_cookie)
    @action(methods=["GET"], detail=False, permission_classes=[AllowAny])
    def session(self, request):
        from django.conf import settings as django_settings
        from django.urls import reverse
        from social_django.context_processors import backends

        from nautobot.core.settings_funcs import sso_auth_enabled

        serializer = self.serializer_class(instance=request.user, context={"request": request})

        _backends = []
        sso_enabled = sso_auth_enabled(django_settings.AUTHENTICATION_BACKENDS)

        social_auth_backends = backends(request)["backends"]
        if sso_enabled:
            for backend in social_auth_backends["backends"]:
                _backends.append(reverse("social:begin", kwargs={"backend": backend}))

        resp = {
            "user": serializer.data,
            "logged_in": request.user.is_authenticated,
            "sso_enabled": sso_enabled,
            "sso_user": (len(social_auth_backends["associated"]) > 0),
            "backends": _backends,
        }

        return Response(resp)

    # TODO(timizuo): Move authenticate and logout to its own view;
    #  as it is not proper to be on this.
    @action(methods=["POST"], detail=False, permission_classes=[AllowAny])
    def authenticate(self, request):
        serializer = serializers.UserLoginSerializer(data=request.data, context=self.get_serializer_context())
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data["user"]
        login(request, user=user)
        return Response(status=200)

    @action(methods=["GET"], detail=False)
    def logout(self, request):
        logout(request)
        return Response(status=200)

from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileView(LoginRequiredMixin, View):
class UserConfigView(LoginRequiredMixin, View):
class ChangePasswordView(LoginRequiredMixin, View):
class TokenListView(LoginRequiredMixin, View):
class TokenEditView(LoginRequiredMixin, View):
class TokenDeleteView(LoginRequiredMixin, View):
class AdvancedProfileSettingsEditView(LoginRequiredMixin, View):
router = OrderedDefaultRouter()
router.APIRootView = views.VirtualizationRootView
from rest_framework.routers import APIRootView


class VirtualizationRootView(APIRootView):
    """
    Virtualization API root view
    """

    def get_view_name(self):
        return "Virtualization"



Fixed code:
Added `nautobot.apps.utils.get_url_for_url_pattern` and `nautobot.apps.utils.get_url_patterns` lookup functions.
Added `nautobot.apps.views.GenericView` base class.
Added support for `view_name` and `view_description` optional parameters when instantiating a `nautobot.apps.api.OrderedDefaultRouter`. Specifying these parameters is to be preferred over defining a custom `APIRootView` subclass when defining App API URLs.
Added requirement for user authentication by default on the `nautobot.apps.api.APIRootView` class. As a consequence, viewing the browsable REST API root endpoints (e.g. `/api/`, `/api/circuits/`, `/api/dcim/`, etc.) now requires user authentication.
Updated example views in the App developer documentation to include `ObjectPermissionRequiredMixin` or `LoginRequiredMixin` as appropriate best practices.
Fixed a 500 error when accessing any of the `/dcim/<port-type>/<uuid>/connect/<termination_b_type>/` view endpoints with an invalid/nonexistent `termination_b_type` string.
Updated custom views in the `example_plugin` to use the new `GenericView` base class as a best practice.
Removed the URL endpoints `/api/users/users/my-profile/`, `/api/users/users/session/`, `/api/users/tokens/authenticate/`, and `/api/users/tokens/logout/` as they are unused at this time.
Added requirement for user authentication to access the endpoint `/extras/job-results/<uuid:pk>/log-table/`; furthermore it will not allow an authenticated user to view log entries for a JobResult they don't otherwise have permission to view. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added narrower permissions enforcement on the endpoints `/extras/git-repositories/<uuid:pk>/sync/` and `/extras/git-repositories/<uuid:pk>/dry-run/`; a user who has `change` permissions for a subset of Git repositories is no longer permitted to sync or dry-run other repositories for which they lack the appropriate permissions. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added narrower permissions enforcement on the `/api/dcim/connected-device/?peer_device=...&?peer_interface=...` REST API endpoint; a user who has `view` permissions for a subset of interfaces is no longer permitted to query other interfaces for which they lack permissions. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added narrower permissions enforcement on all `<app>/<model>/<uuid>/notes/` UI endpoints; a user must now have the appropriate `extras.view_note` permissions to view existing notes. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added requirement for user authentication to access the REST API endpoints `/api/redoc/`, `/api/swagger/`, `/api/swagger.json`, and `/api/swagger.yaml`. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added requirement for user authentication to access the `/api/graphql` REST API endpoint, even when `EXEMPT_VIEW_PERMISSIONS` is configured. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added requirement for user authentication to access the endpoints `/dcim/racks/<uuid>/dynamic-groups/`, `/dcim/devices/<uuid>/dynamic-groups/`, `/ipam/prefixes/<uuid>/dynamic-groups/`, `/ipam/ip-addresses/<uuid>/dynamic-groups/`, `/virtualization/clusters/<uuid>/dynamic-groups/`, and `/virtualization/virtual-machines/<uuid>/dynamic-groups/`, even when `EXEMPT_VIEW_PERMISSIONS` is configured. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
Added requirement for user authentication to access the endpoint `/extras/secrets/provider/<str:provider_slug>/form/`. ([GHSA-m732-wvh2-7cq4](https://github.com/nautobot/nautobot/security/advisories/GHSA-m732-wvh2-7cq4))
router = OrderedDefaultRouter(view_name="Example App")
class ExamplePluginHomeView(views.GenericView):
class ExamplePluginConfigView(views.GenericView):
class ViewToBeOverridden(views.GenericView):
from nautobot.apps.views import GenericView

class ViewOverride(GenericView):
from nautobot.core.api.routers import AuthenticatedAPIRootView as APIRootView, OrderedDefaultRouter
    get_url_for_url_pattern,
    get_url_patterns,
    "get_url_for_url_pattern",
    "get_url_patterns",
    GenericView,
    "GenericView",
router = OrderedDefaultRouter(view_name="Circuits")
import logging

from nautobot.core.api.views import AuthenticatedAPIRootView

logger = logging.getLogger(__name__)

    APIRootView = AuthenticatedAPIRootView

    def __init__(self, *args, view_name=None, view_description=None, **kwargs):
        self.view_name = view_name
        if view_name and not view_description:
            view_description = f"{view_name} API root view"
        self.view_description = view_description


        if issubclass(self.APIRootView, AuthenticatedAPIRootView):
            return self.APIRootView.as_view(
                api_root_dict=api_root_dict, name=self.view_name, description=self.view_description
            )
        # Fallback for the established practice of overriding self.APIRootView with a custom class
        logger.warning(
            "Something has changed an OrderedDefaultRouter's APIRootView attribute to a custom class. "
            "Please verify that class %s implements appropriate authentication controls.",
            self.APIRootView.__name__,
        )
        name = getattr(view, "name", None)
        if name is not None:
            return view.name

from rest_framework import routers, status
from rest_framework.permissions import IsAuthenticated
class AuthenticatedAPIRootView(NautobotAPIVersionMixin, routers.APIRootView):
    Extends DRF's base APIRootView class to enforce user authentication.
    permission_classes = [IsAuthenticated]

    name = None
    description = None


class APIRootView(AuthenticatedAPIRootView):
    """
    This is the root of the REST API.
    API endpoints are arranged by app and model name; e.g. `/api/dcim/locations/`.
    """

    name = "API Root"
    # The code is a stripped down version of graphene-django default View
    # https://github.com/graphql-python/graphene-django/blob/main/graphene_django/views.py#L57

    permission_classes = [IsAuthenticated]
    "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"],
from django.test import tag

from nautobot.core.testing import TestCase
from nautobot.core.utils.lookup import get_url_for_url_pattern, get_url_patterns


@tag("integration")
class AuthenticationEnforcedTestCase(TestCase):
    r"""
    Test that all\* registered views require authentication to access.

    \* with a very small number of known exceptions such as login and logout views.
    """

    def test_all_views_require_authentication(self):
        self.client.logout()
        url_patterns = get_url_patterns()

        for url_pattern in url_patterns:
            with self.subTest(url_pattern=url_pattern):
                url = get_url_for_url_pattern(url_pattern)
                response = self.client.get(url, follow=True)

                if response.status_code == 405:  # Method not allowed
                    response = self.client.post(url, follow=True)

                # Is a view that *should* be open to unauthenticated users?
                if url in [
                    "/admin/login/",
                    "/api/plugins/example-plugin/webhook/",
                    "/health/",
                    "/login/",
                    "/media-failure/",
                    "/template.css",
                ]:
                    self.assertHttpStatus(response, 200, msg=url)
                elif response.status_code == 200:
                    # UI views generally should redirect unauthenticated users to the appropriate login page
                    if url.startswith("/admin"):
                        if "logout" in url:
                            # /admin/logout/ sets next=/admin/ because having login redirect to logout would be silly
                            redirect_url = "/admin/login/?next=/admin/"
                        else:
                            redirect_url = f"/admin/login/?next={url}"
                    else:
                        if "logout" in url:
                            # /logout/ sets next=/ because having login redirect back to logout would be silly
                            redirect_url = "/login/?next=/"
                        else:
                            redirect_url = f"/login/?next={url}"
                    self.assertRedirects(response, redirect_url)
                elif response.status_code != 403:
                    if any(
                        url.startswith(path)
                        for path in [
                            "/complete/",  # social auth
                            "/login/",  # social auth
                            "/media/",  # MEDIA_ROOT
                            "/plugins/example-plugin/docs/",  # STATIC_ROOT
                        ]
                    ):
                        self.assertEqual(response.status_code, 404)
                    else:
                        self.fail(
                            f"Unexpected {response.status_code} response at {url}: "
                            + response.content.decode(response.charset)
                        )
        """Validate unauthenticated users are not able to query anything."""
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    def test_graphql_redirects_back_to_login_unauthenticated(self):
        """Assert that graphql redirects to login page if user is unauthenticated."""
        url = reverse("graphql")
        response = self.client.get(url, follow=True, **headers)
        self.assertHttpStatus(response, 200)
        self.assertRedirects(response, f"/login/?next={url}")
        response_content = response.content.decode(response.charset).replace("\n", "")
        for footer_text in self.footer_elements:
            self.assertNotIn(footer_text, response_content)

    def test_api_docs_403_unauthenticated(self):
        """Assert that api docs return a 403 Forbidden if user is unauthenticated."""
        self.client.logout()
        urls = [
            reverse("api_docs"),
            reverse("api_redocs"),
            reverse("schema"),
            reverse("schema_json"),
            reverse("schema_yaml"),
        ]
            response = self.client.get(url)
            self.assertHttpStatus(response, 403)
import re
from django.urls import get_resolver, URLPattern, URLResolver


def get_url_patterns(urlconf=None, patterns_list=None, base_path="/"):
    """
    Recursively yield a list of registered URL patterns.

    Args:
        urlconf (URLConf): Python module such as `nautobot.core.urls`.
            Default if unspecified is the value of `settings.ROOT_URLCONF`, i.e. the `nautobot.core.urls` module.
        patterns_list (list): Used in recursion. Generally can be omitted on initial call.
            Default if unspecified is the `url_patterns` attribute of the given `urlconf` module.
        base_path (str): String to prepend to all URL patterns yielded.
            Default if unspecified is the string `"/"`.

    Yields:
        (str): Each URL pattern defined in the given urlconf and its descendants

    Examples:
        >>> generator = get_url_patterns()
        >>> next(generator)
        '/'
        >>> next(generator)
        '/search/'
        >>> next(generator)
        '/login/'
        >>> next(generator)
        '/logout/'
        >>> next(generator)
        '/circuits/circuits/<uuid:pk>/terminations/swap/'

        >>> import example_plugin.urls as example_urls
        >>> for url_pattern in get_url_patterns(example_urls, base_path="/plugins/example-plugin/"):
        ...     print(url_pattern)
        ...
        /plugins/example-plugin/
        /plugins/example-plugin/config/
        /plugins/example-plugin/models/<uuid:pk>/dynamic-groups/
        /plugins/example-plugin/other-models/<uuid:pk>/dynamic-groups/
        /plugins/example-plugin/docs/
        /plugins/example-plugin/circuits/<uuid:pk>/example-plugin-tab/
        /plugins/example-plugin/devices/<uuid:pk>/example-plugin-tab-1/
        /plugins/example-plugin/devices/<uuid:pk>/example-plugin-tab-2/
        /plugins/example-plugin/override-target/
        /plugins/example-plugin/^models/$
        /plugins/example-plugin/^models/add/$
        /plugins/example-plugin/^models/import/$
        /plugins/example-plugin/^models/edit/$
        /plugins/example-plugin/^models/delete/$
        /plugins/example-plugin/^models/all-names/$
        /plugins/example-plugin/^models/(?P<pk>[^/.]+)/$
        /plugins/example-plugin/^models/(?P<pk>[^/.]+)/delete/$
        /plugins/example-plugin/^models/(?P<pk>[^/.]+)/edit/$
        /plugins/example-plugin/^models/(?P<pk>[^/.]+)/changelog/$
        /plugins/example-plugin/^models/(?P<pk>[^/.]+)/notes/$
        /plugins/example-plugin/^other-models/$
        /plugins/example-plugin/^other-models/add/$
        /plugins/example-plugin/^other-models/edit/$
        /plugins/example-plugin/^other-models/delete/$
        /plugins/example-plugin/^other-models/(?P<pk>[^/.]+)/$
        /plugins/example-plugin/^other-models/(?P<pk>[^/.]+)/delete/$
        /plugins/example-plugin/^other-models/(?P<pk>[^/.]+)/edit/$
        /plugins/example-plugin/^other-models/(?P<pk>[^/.]+)/changelog/$
        /plugins/example-plugin/^other-models/(?P<pk>[^/.]+)/notes/$
    """
    if urlconf is None:
        urlconf = settings.ROOT_URLCONF
    if patterns_list is None:
        patterns_list = get_resolver(urlconf).url_patterns

    for item in patterns_list:
        if isinstance(item, URLPattern):
            yield base_path + str(item.pattern)
        elif isinstance(item, URLResolver):
            # Recurse!
            yield from get_url_patterns(urlconf, item.url_patterns, base_path + str(item.pattern))


def get_url_for_url_pattern(url_pattern):
    """
    Given a URL pattern, construct a URL string that would match that pattern.

    Examples:
        >>> get_url_for_url_pattern("/plugins/example-plugin/^models/(?P<pk>[^/.]+)/$")
        '/plugins/example-plugin/models/00000000-0000-0000-0000-000000000000/'
        >>> get_url_for_url_pattern("/circuits/circuit-terminations/<uuid:termination_a_id>/connect/<str:termination_b_type>/")
        '/circuits/circuit-terminations/00000000-0000-0000-0000-000000000000/connect/string/'
    """
    url = url_pattern
    # Fixup tokens in path-style "classic" view URLs:
    # "/admin/users/user/<id>/password/"
    url = re.sub(r"<id>", "00000000-0000-0000-0000-000000000000", url)
    # "/silk/request/<uuid:request_id>/profile/<int:profile_id>/"
    url = re.sub(r"<int:\w+>", "1", url)
    # "/admin/admin/logentry/<path:object_id>/"
    url = re.sub(r"<path:\w+>", "1", url)
    # "/dcim/sites/<slug:slug>/"
    url = re.sub(r"<slug:\w+>", "slug", url)
    # "/apps/installed-apps/<str:app>/"
    url = re.sub(r"<str:\w+>", "string", url)
    # "/dcim/locations/<uuid:pk>/"
    url = re.sub(r"<uuid:\w+>", "00000000-0000-0000-0000-000000000000", url)
    # tokens in regexp-style router urls, including REST and NautobotUIViewSet:
    # "/extras/^external-integrations/(?P<pk>[^/.]+)/$"
    # "/api/virtualization/^interfaces/(?P<pk>[^/.]+)/$"
    # "/api/virtualization/^interfaces/(?P<pk>[^/.]+)\\.(?P<format>[a-z0-9]+)/?$"
    url = re.sub(r"[$^]", "", url)
    url = re.sub(r"/\?", "/", url)
    url = re.sub(r"\(\?P<app_label>[^)]+\)", "users", url)
    url = re.sub(r"\(\?P<class_path>[^)]+\)", "foo/bar/baz", url)
    url = re.sub(r"\(\?P<format>[^)]+\)", "json", url)
    url = re.sub(r"\(\?P<name>[^)]+\)", "string", url)
    url = re.sub(r"\(\?P<pk>[^)]+\)", "00000000-0000-0000-0000-000000000000", url)
    url = re.sub(r"\(\?P<slug>[^)]+\)", "string", url)
    url = re.sub(r"\(\?P<url>[^)]+\)", "any", url)
    # Fallthru for generic URL parameters
    url = re.sub(r"\(\?P<\w+>[^)]+\)\??", "unknown", url)
    url = re.sub(r"\\", "", url)

    if any(char in url for char in "<>[]()?+^$"):
        raise RuntimeError(f"Unhandled token in URL {url} derived from {url_pattern}")

    return url
from django.shortcuts import get_object_or_404, render
class StaticMediaFailureView(View):  # NOT using LoginRequiredMixin here as this may happen even on the login page
class CustomGraphQLView(LoginRequiredMixin, GraphQLView):
from django.contrib.auth.mixins import LoginRequiredMixin
class GenericView(LoginRequiredMixin, View):
    """
    Base class for non-object-related views.

    Enforces authentication, which Django's base View does not by default.
    """


router = OrderedDefaultRouter(view_name="DCIM")
            Interface.objects.restrict(request.user, "view"),
        self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
        self.add_permissions("dcim.view_interface")
        response = self.client.get(url + "?peer_device=TestDevice2&peer_interface=eth0", **self.header)
from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
        }.get(kwargs.get("termination_b_type"), None)
        if self.model_form is None:
            return HttpResponse(status_code=400)

from nautobot.apps.views import ObjectPermissionRequiredMixin


class DeviceViewOverride(ObjectPermissionRequiredMixin, generic.View):
    def get_required_permission(self):
        return "dcim.view_device"

from nautobot.apps.views import GenericView
class RandomAnimalView(GenericView):
!!! tip
    The `nautobot.apps.views.GenericView` class was added in Nautobot 1.6.16 and 2.1.9. If you're developing against an earlier version, you can use `django.views.generic.View` in combination with the `django.contrib.auth.mixins.LoginRequiredMixin` instead.

router = OrderedDefaultRouter(view_name="Extras")
from nautobot.core.api.views import AuthenticatedAPIRootView, NautobotAPIVersionMixin
from nautobot.core.views.generic import GenericView
class InstalledPluginDetailView(GenericView):
class PluginsAPIRootView(AuthenticatedAPIRootView):
    name = "Apps"
    description = "API extension point for installed Nautobot Apps"
    LogLevelChoices,
    JobLogEntry,
    def test_get_object_dynamic_groups_anonymous(self):
        url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
        self.client.logout()
        response = self.client.get(url, follow=True)
        self.assertHttpStatus(response, 200)
        self.assertRedirects(response, f"/login/?next={url}")

    def test_get_object_dynamic_groups_without_permission(self):
        url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
        response = self.client.get(url)
        self.assertHttpStatus(response, [403, 404])

    def test_get_object_dynamic_groups_with_permission(self):
        url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
        self.add_permissions("dcim.view_device", "extras.view_dynamicgroup")
        response = self.client.get(url)
        self.assertHttpStatus(response, 200)
        response_body = response.content.decode(response.charset)
        self.assertIn("DG 1", response_body, msg=response_body)
        self.assertIn("DG 2", response_body, msg=response_body)
        self.assertIn("DG 3", response_body, msg=response_body)

    def test_get_object_dynamic_groups_with_constrained_permission(self):
        self.add_permissions("extras.view_dynamicgroup")
        obj_perm = ObjectPermission(
            name="View a device",
            constraints={"pk": Device.objects.first().pk},
            actions=["view"],
        )
        obj_perm.save()
        obj_perm.users.add(self.user)
        obj_perm.object_types.add(ContentType.objects.get_for_model(Device))

        url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
        response = self.client.get(url)
        self.assertHttpStatus(response, 200)
        response_body = response.content.decode(response.charset)
        self.assertIn("DG 1", response_body, msg=response_body)

        url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.last().pk})
        response = self.client.get(url)
        self.assertHttpStatus(response, 404)

    def test_post_sync_repo_anonymous(self):
        self.client.logout()
        url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
        response = self.client.post(url, follow=True)
        self.assertHttpStatus(response, 200)
        self.assertRedirects(response, f"/login/?next={url}")

    def test_post_sync_repo_without_permission(self):
        url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
        response = self.client.post(url)
        self.assertHttpStatus(response, [403, 404])

    # TODO: mock/stub out `enqueue_pull_git_repository_and_refresh_data` and test successful POST with permissions

    def test_post_dryrun_repo_anonymous(self):
        self.client.logout()
        url = reverse("extras:gitrepository_dryrun", kwargs={"pk": self._get_queryset().first().pk})
        response = self.client.post(url, follow=True)
        self.assertHttpStatus(response, 200)
        self.assertRedirects(response, f"/login/?next={url}")

    def test_post_dryrun_repo_without_permission(self):
        url = reverse("extras:gitrepository_dryrun", kwargs={"pk": self._get_queryset().first().pk})
        response = self.client.post(url)
        self.assertHttpStatus(response, [403, 404])

    # TODO: mock/stub out `enqueue_git_repository_diff_origin_and_local` and test successful POST with permissions

        JobLogEntry.objects.create(
            log_level=LogLevelChoices.LOG_INFO,
            job_result=JobResult.objects.first(),
            grouping="run",
            message="This is a test",
        )

    def test_get_joblogentrytable_anonymous(self):
        url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
        self.client.logout()
        response = self.client.get(url, follow=True)
        self.assertHttpStatus(response, 200)
        self.assertRedirects(response, f"/login/?next={url}")

    def test_get_joblogentrytable_without_permission(self):
        url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
        response = self.client.get(url)
        self.assertHttpStatus(response, [403, 404])

    def test_get_joblogentrytable_with_permission(self):
        url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
        self.add_permissions("extras.view_jobresult", "extras.view_joblogentry")
        response = self.client.get(url)
        self.assertHttpStatus(response, 200)
        response_body = response.content.decode(response.charset)
        self.assertIn("This is a test", response_body)

    # TODO test with constrained permissions on both JobResult and JobLogEntry records
class ObjectDynamicGroupsView(generic.GenericView):
        repository = get_object_or_404(GitRepository.objects.restrict(request.user, "change"), pk=pk)
class GitRepositorySyncView(generic.GenericView):
class GitRepositoryDryRunView(generic.GenericView):
class JobLogEntryTableView(generic.GenericView):
        instance = get_object_or_404(self.queryset.restrict(request.user, "view"), pk=pk)
class ObjectChangeLogView(generic.GenericView):
class ObjectNotesView(generic.GenericView):
        notes_table = tables.NoteTable(obj.notes.restrict(request.user, "view"))
class SecretProviderParametersFormView(generic.GenericView):
router = OrderedDefaultRouter(view_name="IPAM")
        self.add_permissions("ipam.view_prefix")
        response = self.client.post(self.api_url, payload, format="json", **self.header)
                return redirect(self.get_return_url(request, default_return_url="ipam:ipaddress_add"))
        if request.user.is_authenticated:
            # Redirect user if an interface has not been provided
            if "interface" not in request.GET and "vminterface" not in request.GET:
                return redirect(self.get_return_url(request, default_return_url="ipam:ipaddress_add"))
            _, error_msg = retrieve_interface_or_vminterface_from_request(request)
            if error_msg:
                messages.warning(request, error_msg)
                return redirect(self.get_return_url(request, default_return_url="ipam:ipaddress_add"))
router = OrderedDefaultRouter(view_name="Tenancy")
router = OrderedDefaultRouter(view_name="Users")
from django.contrib.auth import get_user_model
from rest_framework.permissions import IsAuthenticated
from nautobot.core.views.generic import GenericView
class ProfileView(GenericView):
class UserConfigView(GenericView):
class ChangePasswordView(GenericView):
class TokenListView(GenericView):
class TokenEditView(GenericView):
class TokenDeleteView(GenericView):
class AdvancedProfileSettingsEditView(GenericView):
router = OrderedDefaultRouter(view_name="Virtualization")