Skip to content

Commit f9f0275

Browse files
committed
Merge branch 'master' into feat-rbac
2 parents 80ad86c + 7b1a047 commit f9f0275

File tree

61 files changed

+3642
-189
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3642
-189
lines changed

.github/dependabot.yml

+21
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ updates:
3636
- "dependencies"
3737
- "npm"
3838

39+
- package-ecosystem: "docker"
40+
directory: "/"
41+
schedule:
42+
interval: "weekly"
43+
open-pull-requests-limit: 10
44+
target-branch: master
45+
labels:
46+
- "dependencies"
47+
- "docker"
48+
3949
# v4.6
4050
- package-ecosystem: "pip"
4151
directory: "/"
@@ -59,6 +69,17 @@ updates:
5969
- "github_actions"
6070
- "v4"
6171

72+
- package-ecosystem: "docker"
73+
directory: "/"
74+
schedule:
75+
interval: "weekly"
76+
open-pull-requests-limit: 10
77+
target-branch: v4.6
78+
labels:
79+
- "dependencies"
80+
- "docker"
81+
- "v4"
82+
6283
# v3
6384
- package-ecosystem: "pip"
6485
directory: "/"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: API - Build and Push containers
2+
3+
on:
4+
push:
5+
branches:
6+
- "master"
7+
paths:
8+
- "api/**"
9+
- ".github/workflows/api-build-lint-push-containers.yml"
10+
11+
# Uncomment the code below to test this action on PRs
12+
# pull_request:
13+
# branches:
14+
# - "master"
15+
# paths:
16+
# - "api/**"
17+
# - ".github/workflows/api-build-lint-push-containers.yml"
18+
19+
release:
20+
types: [published]
21+
22+
env:
23+
# Tags
24+
LATEST_TAG: latest
25+
RELEASE_TAG: ${{ github.event.release.tag_name }}
26+
27+
WORKING_DIRECTORY: ./api
28+
29+
# Container Registries
30+
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
31+
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
32+
33+
jobs:
34+
# Build Prowler OSS container
35+
container-build-push:
36+
runs-on: ubuntu-latest
37+
defaults:
38+
run:
39+
working-directory: ${{ env.WORKING_DIRECTORY }}
40+
41+
steps:
42+
- name: Repository check
43+
working-directory: /tmp
44+
run: |
45+
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
46+
47+
- name: Checkout
48+
uses: actions/checkout@v4
49+
50+
- name: Login to DockerHub
51+
uses: docker/login-action@v3
52+
with:
53+
username: ${{ secrets.DOCKERHUB_USERNAME }}
54+
password: ${{ secrets.DOCKERHUB_TOKEN }}
55+
56+
- name: Set up Docker Buildx
57+
uses: docker/setup-buildx-action@v3
58+
59+
- name: Build and push container image (latest)
60+
# Comment the following line for testing
61+
if: github.event_name == 'push'
62+
uses: docker/build-push-action@v6
63+
with:
64+
context: ${{ env.WORKING_DIRECTORY }}
65+
# Set push: false for testing
66+
push: true
67+
tags: |
68+
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
69+
cache-from: type=gha
70+
cache-to: type=gha,mode=max
71+
72+
- name: Build and push container image (release)
73+
if: github.event_name == 'release'
74+
uses: docker/build-push-action@v6
75+
with:
76+
context: ${{ env.WORKING_DIRECTORY }}
77+
push: true
78+
tags: |
79+
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
80+
cache-from: type=gha
81+
cache-to: type=gha,mode=max

.github/workflows/find-secrets.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
with:
1212
fetch-depth: 0
1313
- name: TruffleHog OSS
14-
uses: trufflesecurity/trufflehog@v3.85.0
14+
uses: trufflesecurity/trufflehog@v3.86.1
1515
with:
1616
path: ./
1717
base: ${{ github.event.repository.default_branch }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: UI - Build and Push containers
2+
3+
on:
4+
push:
5+
branches:
6+
- "master"
7+
paths:
8+
- "ui/**"
9+
- ".github/workflows/ui-build-lint-push-containers.yml"
10+
11+
# Uncomment the below code to test this action on PRs
12+
# pull_request:
13+
# branches:
14+
# - "master"
15+
# paths:
16+
# - "ui/**"
17+
# - ".github/workflows/ui-build-lint-push-containers.yml"
18+
19+
release:
20+
types: [published]
21+
22+
env:
23+
# Tags
24+
LATEST_TAG: latest
25+
RELEASE_TAG: ${{ github.event.release.tag_name }}
26+
27+
WORKING_DIRECTORY: ./ui
28+
29+
# Container Registries
30+
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
31+
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
32+
33+
jobs:
34+
# Build Prowler OSS container
35+
container-build-push:
36+
runs-on: ubuntu-latest
37+
defaults:
38+
run:
39+
working-directory: ${{ env.WORKING_DIRECTORY }}
40+
41+
steps:
42+
- name: Repository check
43+
working-directory: /tmp
44+
run: |
45+
[[ ${{ github.repository }} != "prowler-cloud/prowler" ]] && echo "This action only runs for prowler-cloud/prowler"; exit 0
46+
47+
- name: Checkout
48+
uses: actions/checkout@v4
49+
50+
- name: Login to DockerHub
51+
uses: docker/login-action@v3
52+
with:
53+
username: ${{ secrets.DOCKERHUB_USERNAME }}
54+
password: ${{ secrets.DOCKERHUB_TOKEN }}
55+
56+
- name: Set up Docker Buildx
57+
uses: docker/setup-buildx-action@v3
58+
59+
- name: Build and push container image (latest)
60+
# Comment the following line for testing
61+
if: github.event_name == 'push'
62+
uses: docker/build-push-action@v6
63+
with:
64+
context: ${{ env.WORKING_DIRECTORY }}
65+
# Set push: false for testing
66+
push: true
67+
tags: |
68+
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
69+
cache-from: type=gha
70+
cache-to: type=gha,mode=max
71+
72+
- name: Build and push container image (release)
73+
if: github.event_name == 'release'
74+
uses: docker/build-push-action@v6
75+
with:
76+
context: ${{ env.WORKING_DIRECTORY }}
77+
push: true
78+
tags: |
79+
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
80+
cache-from: type=gha
81+
cache-to: type=gha,mode=max

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<p align="center">
3030
<a href="https://github.com/prowler-cloud/prowler"><img alt="Repo size" src="https://img.shields.io/github/repo-size/prowler-cloud/prowler"></a>
3131
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
32-
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler?include_prereleases"></a>
32+
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
3333
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/release-date/prowler-cloud/prowler"></a>
3434
<a href="https://github.com/prowler-cloud/prowler"><img alt="Contributors" src="https://img.shields.io/github/contributors-anon/prowler-cloud/prowler"></a>
3535
<a href="https://github.com/prowler-cloud/prowler"><img alt="License" src="https://img.shields.io/github/license/prowler-cloud/prowler"></a>

api/src/backend/api/base_views.py

+6-27
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import uuid
2-
31
from django.core.exceptions import ObjectDoesNotExist
4-
from django.db import connection, transaction
2+
from django.db import transaction
53
from rest_framework import permissions
64
from rest_framework.exceptions import NotAuthenticated
75
from rest_framework.filters import SearchFilter
86
from rest_framework_json_api import filters
9-
from rest_framework_json_api.serializers import ValidationError
107
from rest_framework_json_api.views import ModelViewSet
118
from rest_framework_simplejwt.authentication import JWTAuthentication
129

10+
from api.db_utils import POSTGRES_USER_VAR, tenant_transaction
1311
from api.filters import CustomDjangoFilterBackend
1412
from api.models import Role, Tenant
1513
from api.db_router import MainRouter
@@ -50,13 +48,7 @@ def initial(self, request, *args, **kwargs):
5048
if tenant_id is None:
5149
raise NotAuthenticated("Tenant ID is not present in token")
5250

53-
try:
54-
uuid.UUID(tenant_id)
55-
except ValueError:
56-
raise ValidationError("Tenant ID must be a valid UUID")
57-
58-
with connection.cursor() as cursor:
59-
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
51+
with tenant_transaction(tenant_id):
6052
self.request.tenant_id = tenant_id
6153
return super().initial(request, *args, **kwargs)
6254

@@ -110,8 +102,7 @@ def initial(self, request, *args, **kwargs):
110102
):
111103
user_id = str(request.user.id)
112104

113-
with connection.cursor() as cursor:
114-
cursor.execute(f"SELECT set_config('api.user_id', '{user_id}', TRUE);")
105+
with tenant_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
115106
return super().initial(request, *args, **kwargs)
116107

117108
# TODO: DRY this when we have time
@@ -122,13 +113,7 @@ def initial(self, request, *args, **kwargs):
122113
if tenant_id is None:
123114
raise NotAuthenticated("Tenant ID is not present in token")
124115

125-
try:
126-
uuid.UUID(tenant_id)
127-
except ValueError:
128-
raise ValidationError("Tenant ID must be a valid UUID")
129-
130-
with connection.cursor() as cursor:
131-
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
116+
with tenant_transaction(tenant_id):
132117
self.request.tenant_id = tenant_id
133118
return super().initial(request, *args, **kwargs)
134119

@@ -149,12 +134,6 @@ def initial(self, request, *args, **kwargs):
149134
if tenant_id is None:
150135
raise NotAuthenticated("Tenant ID is not present in token")
151136

152-
try:
153-
uuid.UUID(tenant_id)
154-
except ValueError:
155-
raise ValidationError("Tenant ID must be a valid UUID")
156-
157-
with connection.cursor() as cursor:
158-
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
137+
with tenant_transaction(tenant_id):
159138
self.request.tenant_id = tenant_id
160139
return super().initial(request, *args, **kwargs)

api/src/backend/api/db_utils.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import secrets
2+
import uuid
23
from contextlib import contextmanager
34
from datetime import datetime, timedelta, timezone
45

@@ -8,6 +9,7 @@
89
from django.db import connection, models, transaction
910
from psycopg2 import connect as psycopg2_connect
1011
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
12+
from rest_framework_json_api.serializers import ValidationError
1113

1214
DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
1315
DB_PASSWORD = (
@@ -23,6 +25,8 @@
2325
POSTGRES_TENANT_VAR = "api.tenant_id"
2426
POSTGRES_USER_VAR = "api.user_id"
2527

28+
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
29+
2630

2731
@contextmanager
2832
def psycopg_connection(database_alias: str):
@@ -44,10 +48,23 @@ def psycopg_connection(database_alias: str):
4448

4549

4650
@contextmanager
47-
def tenant_transaction(tenant_id: str):
51+
def tenant_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
52+
"""
53+
Creates a new database transaction setting the given configuration value. It validates the
54+
if the value is a valid UUID to be used for Postgres RLS.
55+
56+
Args:
57+
value (str): Database configuration parameter value.
58+
parameter (str): Database configuration parameter name
59+
"""
4860
with transaction.atomic():
4961
with connection.cursor() as cursor:
50-
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
62+
try:
63+
# just in case the value is an UUID object
64+
uuid.UUID(str(value))
65+
except ValueError:
66+
raise ValidationError("Must be a valid UUID")
67+
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
5168
yield cursor
5269

5370

api/src/backend/api/decorators.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import uuid
12
from functools import wraps
23

34
from django.db import connection, transaction
5+
from rest_framework_json_api.serializers import ValidationError
6+
7+
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
48

59

610
def set_tenant(func):
@@ -31,7 +35,7 @@ def some_task(arg1, **kwargs):
3135
pass
3236
3337
# When calling the task
34-
some_task.delay(arg1, tenant_id="1234-abcd-5678")
38+
some_task.delay(arg1, tenant_id="8db7ca86-03cc-4d42-99f6-5e480baf6ab5")
3539
3640
# The tenant context will be set before the task logic executes.
3741
"""
@@ -43,9 +47,12 @@ def wrapper(*args, **kwargs):
4347
tenant_id = kwargs.pop("tenant_id")
4448
except KeyError:
4549
raise KeyError("This task requires the tenant_id")
46-
50+
try:
51+
uuid.UUID(tenant_id)
52+
except ValueError:
53+
raise ValidationError("Tenant ID must be a valid UUID")
4754
with connection.cursor() as cursor:
48-
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
55+
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
4956

5057
return func(*args, **kwargs)
5158

api/src/backend/api/tests/test_decorators.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from unittest.mock import patch, call
1+
import uuid
2+
from unittest.mock import call, patch
23

34
import pytest
45

6+
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
57
from api.decorators import set_tenant
68

79

@@ -15,12 +17,12 @@ def test_set_tenant(self, mock_cursor):
1517
def random_func(arg):
1618
return arg
1719

18-
tenant_id = "1234-abcd-5678"
20+
tenant_id = str(uuid.uuid4())
1921

2022
result = random_func("test_arg", tenant_id=tenant_id)
2123

2224
assert (
23-
call(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
25+
call(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
2426
in mock_cursor.execute.mock_calls
2527
)
2628
assert result == "test_arg"

0 commit comments

Comments
 (0)