mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
feat(User): Add GET /users (#76)
* feat(Membership): implement include parameter for users in memberships * feat(User): refactor userviewset * chore(Schema): update API schema * feat(User): add filters
This commit is contained in:
committed by
GitHub
parent
6f1aa6a1b1
commit
0271fe5ca0
@@ -74,3 +74,30 @@ class BaseTenantViewset(BaseViewSet):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.user_id', '{user_id}', TRUE);")
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseUserViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# TODO refactor after improving RLS on users
|
||||
if request.stream is not None and request.stream.method == "POST":
|
||||
return super().initial(request, *args, **kwargs)
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
tenant_id = request.auth.get("tenant_id")
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -22,6 +22,7 @@ from api.db_utils import (
|
||||
InvitationStateEnumField,
|
||||
)
|
||||
from api.models import (
|
||||
User,
|
||||
Membership,
|
||||
Provider,
|
||||
Resource,
|
||||
@@ -434,3 +435,17 @@ class InvitationFilter(FilterSet):
|
||||
"filter_class": CharFilter,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
date_joined = DateFilter(field_name="date_joined", lookup_expr="date")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
"name": ["exact", "icontains"],
|
||||
"email": ["exact", "icontains"],
|
||||
"company_name": ["exact", "icontains"],
|
||||
"date_joined": ["date", "gte", "lte"],
|
||||
"is_active": ["exact"],
|
||||
}
|
||||
|
||||
@@ -2622,6 +2622,124 @@ paths:
|
||||
$ref: '#/components/schemas/TokenRefreshResponse'
|
||||
description: ''
|
||||
/api/v1/users:
|
||||
get:
|
||||
operationId: users_list
|
||||
description: Retrieve a list of all users with options for filtering by various
|
||||
criteria.
|
||||
summary: List all users
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[User]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- name
|
||||
- email
|
||||
- company_name
|
||||
- date_joined
|
||||
- memberships
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: filter[company_name]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[company_name__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[date_joined]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[date_joined__date]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[date_joined__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[date_joined__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[email]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[email__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[is_active]
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter[name]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[name__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- name: page[number]
|
||||
required: false
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
schema:
|
||||
type: integer
|
||||
- name: page[size]
|
||||
required: false
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- name
|
||||
- -name
|
||||
- email
|
||||
- -email
|
||||
- company_name
|
||||
- -company_name
|
||||
- date_joined
|
||||
- -date_joined
|
||||
- is_active
|
||||
- -is_active
|
||||
explode: false
|
||||
tags:
|
||||
- User
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedUserList'
|
||||
description: ''
|
||||
post:
|
||||
operationId: users_create
|
||||
description: Create a new user account by providing the necessary registration
|
||||
@@ -2661,8 +2779,7 @@ paths:
|
||||
/api/v1/users/{id}:
|
||||
get:
|
||||
operationId: users_retrieve
|
||||
description: Fetch detailed information about an authenticated user. It only
|
||||
allows using your own user ID.
|
||||
description: Fetch detailed information about an authenticated user.
|
||||
summary: Retrieve a user's information
|
||||
parameters:
|
||||
- in: query
|
||||
@@ -3586,6 +3703,15 @@ components:
|
||||
$ref: '#/components/schemas/Tenant'
|
||||
required:
|
||||
- data
|
||||
PaginatedUserList:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
required:
|
||||
- data
|
||||
PatchedInvitationUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -21,9 +21,18 @@ TODAY = str(datetime.today().date())
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestUserViewSet:
|
||||
def test_users_list_not_allowed(self, authenticated_client):
|
||||
def test_users_list(self, authenticated_client, create_test_user):
|
||||
user = create_test_user
|
||||
user.refresh_from_db()
|
||||
response = authenticated_client.get(reverse("user-list"))
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
assert response.json()["data"][0]["attributes"]["email"] == user.email
|
||||
assert response.json()["data"][0]["attributes"]["name"] == user.name
|
||||
assert (
|
||||
response.json()["data"][0]["attributes"]["company_name"]
|
||||
== user.company_name
|
||||
)
|
||||
|
||||
def test_users_retrieve(self, authenticated_client, create_test_user):
|
||||
response = authenticated_client.get(
|
||||
|
||||
+15
-17
@@ -29,7 +29,7 @@ from rest_framework_json_api.views import Response
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
from rest_framework_simplejwt.exceptions import TokenError
|
||||
|
||||
from api.base_views import BaseTenantViewset, BaseRLSViewSet, BaseViewSet
|
||||
from api.base_views import BaseTenantViewset, BaseRLSViewSet, BaseUserViewset
|
||||
from api.db_router import MainRouter
|
||||
from api.filters import (
|
||||
ProviderFilter,
|
||||
@@ -41,6 +41,7 @@ from api.filters import (
|
||||
FindingFilter,
|
||||
ProviderSecretFilter,
|
||||
InvitationFilter,
|
||||
UserFilter,
|
||||
)
|
||||
from api.models import (
|
||||
User,
|
||||
@@ -196,9 +197,13 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
summary="List all users",
|
||||
description="Retrieve a list of all users with options for filtering by various criteria.",
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
summary="Retrieve a user's information",
|
||||
description="Fetch detailed information about an authenticated user. It only allows using your own user ID.",
|
||||
description="Fetch detailed information about an authenticated user.",
|
||||
),
|
||||
create=extend_schema(
|
||||
summary="Register a new user",
|
||||
@@ -218,14 +223,18 @@ class SchemaView(SpectacularAPIView):
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
class UserViewSet(BaseViewSet):
|
||||
class UserViewSet(BaseUserViewset):
|
||||
serializer_class = UserSerializer
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
ordering = ["id"]
|
||||
ordering_fields = []
|
||||
filterset_class = UserFilter
|
||||
ordering = ["-date_joined"]
|
||||
ordering_fields = ["name", "email", "company_name", "date_joined", "is_active"]
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(id=self.request.user.id)
|
||||
# If called during schema generation, return an empty queryset
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return User.objects.none()
|
||||
return User.objects.filter(membership__tenant__id=self.request.tenant_id)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "create":
|
||||
@@ -242,15 +251,6 @@ class UserViewSet(BaseViewSet):
|
||||
else:
|
||||
return UserSerializer
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def list(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(method="GET")
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if kwargs["pk"] != str(request.user.id):
|
||||
raise NotFound(detail="User was not found.")
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="me")
|
||||
def me(self, request):
|
||||
user = self.get_queryset().first()
|
||||
@@ -445,8 +445,6 @@ class TenantMembersViewSet(BaseTenantViewset):
|
||||
raise NotFound("Membership does not exist.")
|
||||
return membership
|
||||
|
||||
# TODO: Add invite functionality
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(method="GET")
|
||||
|
||||
Reference in New Issue
Block a user