Got 403 when request an API URL

Hi. I created a django app with
settings.py

"""
Django settings for restapi project.

Generated by 'django-admin startproject' using Django 5.2.

For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-b0jt)bxzsmm-#8a7zbpr0=3xe!kh4vqu80(gnn22-=#h9x*=)#'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["*"]


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api.apps.ApiConfig',
    'drf_spectacular',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'restapi.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'restapi.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

views.py

from django.contrib.auth.models import Group, User
from rest_framework import permissions, viewsets
from rest_framework.exceptions import PermissionDenied

from .serializers import RoleSerializer, UserSerializer, ContestSerializer, PhotoSerializer, VoteSerializer
from api.models import Contest, Photo, Vote


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAdminUser]

    # Create a new user and set the password if provided.
    def perform_create(self, serializer):
        user = serializer.save()
        user.set_password(self.request.data['password'])
        user.save()

class RoleViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows roles to be viewed or edited.
    """
    queryset = Group.objects.all().order_by('name')
    serializer_class = RoleSerializer
    permission_classes = [permissions.IsAdminUser]


class ContestViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows contests to be viewed or edited.
    """
    queryset = Contest.objects.all().order_by('name')
    serializer_class = ContestSerializer
    permission_classes = [permissions.IsAdminUser]


class PhotoViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows photos to be viewed or edited.
    """
    queryset = Photo.objects.all().order_by('upload_date')
    serializer_class = PhotoSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

    def perform_update(self, serializer, *args, **kwargs):
        UPDATE_ERROR_MSG = "You cannot update this photo because you are not the owner."
        if self.request.user != serializer.owner:
            raise PermissionDenied(UPDATE_ERROR_MSG)
        return super().perform_update(serializer)

    def perform_destroy(self, serializer, *args, **kwargs):
        DELETE_ERROR_MSG = "You cannot delete this photo because you are not the owner."
        if self.request.user != serializer.owner:
            raise PermissionDenied(DELETE_ERROR_MSG)
        return super().perform_destroy(serializer)


class VoteViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows votes to be viewed or edited.
    """
    queryset = Vote.objects.all().order_by('timestamp')
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

    def perform_update(self, serializer, *args, **kwargs):
        UPDATE_ERROR_MSG = "You cannot update this vote because you are not the owner."
        if self.request.user != serializer:
            raise PermissionDenied(UPDATE_ERROR_MSG)
        return super().perform_update(serializer)

    def perform_destroy(self, serializer, *args, **kwargs):
        DELETE_ERROR_MSG = "You cannot delete this vote because you are not the owner."
        if self.request.user != serializer:
            raise PermissionDenied(DELETE_ERROR_MSG)
        return super().perform_destroy(serializer)

And testing with a client, autogenerated with the schema with the tool https://openapi-generator.tech/ , I got 403 when request get to /votes/ or /photos/.

[1399] NetworkUtility.shouldRetryException: Unexpected response code 403 for http://10.0.2.2:8000/votes/
[1399] NetworkUtility.shouldRetryException: Unexpected response code 403 for http://10.0.2.2:8000/votes/
ApiException{code=403, message=null}
	at client.api.VotesApi.votesList(VotesApi.java:361)
	at .LaunchActivity.lambda$test$3$es-alejandromarmol-rallyfotografico-LaunchActivity(LaunchActivity.java:53)
	at .LaunchActivity$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)
	at java.lang.Thread.run(Thread.java:923)

Which I have the log:

Forbidden: /votes/
[22/Apr/2025 13:00:19] "GET /votes/ HTTP/1.1" 403 39
Forbidden: /votes/
[22/Apr/2025 13:00:19] "GET /votes/ HTTP/1.1" 403 39

Basically I need to know why, it’s supposed to allow me to request get anonymously, and my API client should be well autogenerated. Please check if CORS or anything is required, but other error would be raised, right?

Where is urls.py? To what view /votes are pointed?

How does the view know to show a specific vote? How the view know which vote to delete/edit?

Sorry, here you have it

"""
URL configuration for restapi project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from . import views
from django.conf import settings
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

router = routers.DefaultRouter()

router.register(r'users', views.UserViewSet)
router.register(r'roles', views.RoleViewSet)
router.register(r'contests', views.ContestViewSet)
router.register(r'photos', views.PhotoViewSet)
router.register(r'votes', views.VoteViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(router.urls)),
    path('auth/', include('rest_framework.urls')),
    path('schema/', SpectacularAPIView.as_view(), name='schema'),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)