Skip to content

Matomo #769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 58 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
b43ca4a
Fix matomo proxy
aloftus23 Jan 17, 2025
35dfacf
FIx matomo proxy
aloftus23 Jan 21, 2025
bf902e4
Merge branch 'develop' of github.com:cisagov/XFD into matomo
Matthew-Grayson Jan 22, 2025
6c679ce
Added logging to matomodb
chrtorres Jan 23, 2025
c30c8f9
Added logging to matomo container
chrtorres Jan 28, 2025
8baaeff
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Jan 28, 2025
a4f2b18
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Feb 3, 2025
a14a3a9
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Feb 10, 2025
e95ce4a
removed user requirement from matomo proxy; Removed cookie index refe…
chrtorres Feb 10, 2025
1d0592e
Move Matomo tracking script to separate file; make all Matomo proxy p…
Matthew-Grayson Feb 18, 2025
a190fa8
Merge branch 'matomo' of github.com:cisagov/XFD into matomo
Matthew-Grayson Feb 18, 2025
54a2e80
Testing matomo proxy redirect in views.py
chrtorres Feb 18, 2025
df91f5b
Merge remote-tracking branch 'origin/matomo' into CT-matomo-proxy-fix
chrtorres Feb 18, 2025
a16f437
Added matomo redirect /index.php, and logo redirect; Removed cookie_n…
chrtorres Feb 24, 2025
2669865
Changed matomo proxy redirect status code to 308 Permanent redirect
chrtorres Feb 24, 2025
2cb644b
Merge remote-tracking branch 'origin/matomo' into CT-matomo-proxy-fix
chrtorres Feb 26, 2025
5f9a912
Added code to conditionally set the CSP for Matomo routes
chrtorres Mar 5, 2025
5431fb4
Merge remote-tracking branch 'origin' into matomo
chrtorres Mar 5, 2025
5959237
Merge branch 'matomo' into CT-matomo-proxy-fix
chrtorres Mar 5, 2025
2c49ea8
Created Analytics Admin role; Granted Global View permissions to anal…
chrtorres Mar 10, 2025
16ca2d4
added Analytics to userType on CyHy Dashboard
lwersiy Mar 27, 2025
46d32dc
Merge remote-tracking branch 'origin' into matomo
chrtorres Mar 28, 2025
e971878
Merge remote-tracking branch 'origin/matomo' into CT-matomo-proxy-fix
chrtorres Mar 28, 2025
4475062
Modified route check for Matomo CSP; removed unnecessary routes withi…
chrtorres Apr 2, 2025
3d13681
Modified Routeguard permissions to allow analytics admin access
chrtorres Apr 3, 2025
53a3756
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Apr 4, 2025
08831b8
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Apr 7, 2025
1da7934
Merge remote-tracking branch 'origin/matomo' into CT-matomo-proxy-fix
chrtorres Apr 7, 2025
098f1e6
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Apr 9, 2025
cad4783
Merge remote-tracking branch 'origin/matomo' into CT-matomo-proxy-fix
chrtorres Apr 9, 2025
c525957
Corrected linting errors; Added access to user.py for Analytics Admin
chrtorres Apr 9, 2025
17453fb
Corrected stray linting errors in views.py and asgi.py
chrtorres Apr 10, 2025
08607a6
Code cleanup
chrtorres Apr 10, 2025
9051782
Fix linting errors
chrtorres Apr 10, 2025
422e505
Added user level for Analytics admin role; Added filter options for a…
chrtorres Apr 10, 2025
b343064
Added Analytics permissions to front end
chrtorres Apr 11, 2025
7fa97bf
Removed unused comment
chrtorres Apr 11, 2025
e3e8793
Refactored Analytics Admin to Analytics
chrtorres Apr 11, 2025
4983927
Merge remote-tracking branch 'origin' into matomo
chrtorres Apr 11, 2025
37757ac
Merge branch 'matomo' into CT-matomo-proxy-fix
chrtorres Apr 11, 2025
bef8286
Merge pull request #848 from cisagov/CT-matomo-proxy-fix
rapidray12 Apr 11, 2025
b33da59
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Apr 16, 2025
60a2a66
Refactored Matomo Security Policyto prevent access
chrtorres Apr 21, 2025
0df00ae
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Apr 21, 2025
0ce5b89
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres Apr 21, 2025
cc71ea7
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres May 6, 2025
25642f7
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres May 8, 2025
00fa28b
Modified permissions to allow analytics users access to: All Vulns, A…
chrtorres May 13, 2025
144189c
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres May 15, 2025
1968104
Merge branch 'matomo' into CT-CRASM-2144-Analytics-Role
chrtorres May 15, 2025
794c439
Restriced permissions to disable approve/deny buttons within Region a…
chrtorres May 15, 2025
809d255
Code cleanup: Removed stray print statements
chrtorres May 15, 2025
a1102ef
Modified permissions to Manage Orgs to prevent analytics users from e…
chrtorres May 19, 2025
67e5868
Merge remote-tracking branch 'origin/develop' into matomo
chrtorres May 19, 2025
ce812a7
Merge branch 'CT-CRASM-2144-Analytics-Role' into matomo
chrtorres May 19, 2025
8e31435
Revert matomo.js source in index.html
chrtorres May 19, 2025
8f61f04
Removed matomo.js from frontend to call through backend api instead
chrtorres May 19, 2025
dcaff61
Removed useless docstring and prints used for debugging
chrtorres May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/src/xfd_django/xfd_api/api_methods/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from fastapi import HTTPException
from xfd_mini_dl.models import Domain, Service

from ..auth import get_org_memberships, is_global_view_admin
from ..auth import get_org_memberships, is_analytics_user, is_global_view_admin
from ..helpers.filter_helpers import apply_domain_filters, sort_direction
from ..helpers.s3_client import S3Client
from ..schema_models.domain import DomainSearch
Expand Down Expand Up @@ -98,7 +98,7 @@ def search_domains(domain_search: DomainSearch, current_user):
)

# Apply global user permission filters
if not is_global_view_admin(current_user):
if not is_global_view_admin(current_user) | is_analytics_user(current_user):
orgs = get_org_memberships(current_user)
if not orgs:
# No organization memberships, return empty result
Expand Down
36 changes: 25 additions & 11 deletions backend/src/xfd_django/xfd_api/api_methods/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from ..auth import (
get_org_memberships,
is_analytics_user,
is_global_view_admin,
is_global_write_admin,
is_org_admin,
Expand Down Expand Up @@ -42,14 +43,18 @@ def list_organizations(current_user):
"""List organizations that the user is a member of or has access to."""
try:
# Check if user is GlobalViewAdmin or has memberships
if not is_global_view_admin(current_user) and not get_org_memberships(
current_user
if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not get_org_memberships(current_user)
):
return []

# Define filter for organizations based on admin status
org_filter = {}
if not is_global_view_admin(current_user):
if not is_global_view_admin(current_user) and not is_analytics_user(
current_user
):
org_filter["id__in"] = get_org_memberships(current_user)
org_filter["parent"] = None

Expand Down Expand Up @@ -128,9 +133,10 @@ def get_organization(organization_id, current_user):
try:
# Authorization checks
if not (
is_analytics_user(current_user),
is_org_admin(current_user, organization_id)
or is_global_view_admin(current_user)
or is_regional_admin_for_organization(current_user, organization_id)
or is_regional_admin_for_organization(current_user, organization_id),
):
raise HTTPException(status_code=403, detail="Unauthorized")

Expand Down Expand Up @@ -337,8 +343,10 @@ def get_all_regions(current_user):
"""Get all regions."""
try:
# Check if user is GlobalViewAdmin or has memberships
if not is_global_view_admin(current_user) and not get_org_memberships(
current_user
if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not get_org_memberships(current_user)
):
raise HTTPException(status_code=403, detail="Unauthorized")

Expand Down Expand Up @@ -997,15 +1005,19 @@ def list_organizations_v2(state, region_id, current_user):
"""List organizations that the user is a member of or has access to."""
try:
# Check if user is GlobalViewAdmin or has memberships
if not is_global_view_admin(current_user) and not get_org_memberships(
current_user
if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not get_org_memberships(current_user)
):
return []

# Prepare the filter criteria
filter_criteria = Q()

if not is_global_view_admin(current_user):
if not is_global_view_admin(current_user) and not is_analytics_user(
current_user
):
filter_criteria &= Q(id__in=get_org_memberships(current_user))

if state:
Expand Down Expand Up @@ -1066,8 +1078,10 @@ def search_organizations_task(search_body, current_user: User):
"""Handle the logic for searching organizations in Elasticsearch."""
try:
# Check if user is GlobalViewAdmin or has memberships
if not is_global_view_admin(current_user) and not get_org_memberships(
current_user
if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not get_org_memberships(current_user)
):
return []

Expand Down
57 changes: 38 additions & 19 deletions backend/src/xfd_django/xfd_api/api_methods/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,70 @@

# Third-Party Libraries
from fastapi import Request
from fastapi.responses import Response
from fastapi.responses import RedirectResponse, Response
import httpx


# Helper function to handle cookie manipulation
def manipulate_cookie(request: Request, cookie_name: str):
"""Manipulate cookie."""
cookies = request.cookies.get(cookie_name)
if cookies:
return {cookie_name: cookies}
return {}


# Helper function to proxy requests
async def proxy_request(
request: Request,
target_url: str,
path: Optional[str] = None,
cookie_name: Optional[str] = None,
):
"""Proxy the request to the target URL."""
"""Proxy requests to the specified target URL with optional cookie handling."""
print("Proxying request to target URL: {}".format(target_url))
headers = dict(request.headers)

# Cookie manipulation for specific cookie names
# Include specified cookie in the headers if present
if cookie_name:
cookies = manipulate_cookie(request, cookie_name)
print("Cookie name: {}".format(cookie_name))
cookies = request.cookies.get(cookie_name)
if cookies:
headers["Cookie"] = "{}={}".format(cookie_name, cookies[cookie_name])
headers["Cookie"] = "{}={}".format(cookie_name, cookies)

# Make the request to the target URL
async with httpx.AsyncClient() as client:
# Send the request to the target
async with httpx.AsyncClient(timeout=httpx.Timeout(90.0)) as client:
proxy_response = await client.request(
method=request.method,
url="{}/{}".format(target_url, path),
headers=headers,
params=request.query_params,
content=await request.body(),
)

# Remove chunked encoding for API Gateway compatibility
# Adjust response headers
proxy_response_headers = dict(proxy_response.headers)
proxy_response_headers.pop("transfer-encoding", None)
for header in ["content-encoding", "transfer-encoding", "content-length"]:
proxy_response_headers.pop(header, None)

return Response(
content=proxy_response.content,
status_code=proxy_response.status_code,
headers=proxy_response_headers,
)


async def matomo_proxy_handler(
request: Request,
path: str,
MATOMO_URL: str,
):
"""
Handle Matomo-specific proxy logic.

Includes public paths, font redirects, and authentication for private paths.
"""
# Redirect font requests to CDN
font_paths = {
"/plugins/Morpheus/fonts/matomo.woff2": "https://cdn.jsdelivr.net/gh/matomo-org/[email protected]/plugins/Morpheus/fonts/matomo.woff2",
"/plugins/Morpheus/fonts/matomo.woff": "https://cdn.jsdelivr.net/gh/matomo-org/[email protected]/plugins/Morpheus/fonts/matomo.woff",
"/plugins/Morpheus/fonts/matomo.ttf": "https://cdn.jsdelivr.net/gh/matomo-org/[email protected]/plugins/Morpheus/fonts/matomo.ttf",
}
if path in font_paths:
return RedirectResponse(url=font_paths[path])

return await proxy_request(
request=request,
target_url=MATOMO_URL,
path=path,
)
5 changes: 3 additions & 2 deletions backend/src/xfd_django/xfd_api/api_methods/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from xfd_api.auth import (
get_org_memberships,
get_tag_organizations,
is_analytics_user,
is_global_view_admin,
)
from xfd_api.helpers.elastic_search import build_request
Expand All @@ -22,7 +23,7 @@ async def get_options(search_body, user) -> Dict[str, Any]:
"""Get Elastic Search options."""
if search_body.organization_id and (
search_body.organization_id in get_org_memberships(user)
or is_global_view_admin(user)
or (is_global_view_admin(user) | is_analytics_user(user))
):
return {
"organization_ids": [search_body.organization_id],
Expand All @@ -36,7 +37,7 @@ async def get_options(search_body, user) -> Dict[str, Any]:

return {
"organization_ids": get_org_memberships(user),
"match_all_organizations": is_global_view_admin(user),
"match_all_organizations": is_global_view_admin(user) | is_analytics_user(user),
}


Expand Down
8 changes: 5 additions & 3 deletions backend/src/xfd_django/xfd_api/api_methods/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
VulnScanSummary,
)

from ..auth import get_org_memberships, is_global_view_admin
from ..auth import get_org_memberships, is_analytics_user, is_global_view_admin


# GET: /stats
Expand Down Expand Up @@ -109,7 +109,8 @@ async def safe_fetch(fetch_fn, *args, **kwargs):
}
except Exception as e:
raise HTTPException(
status_code=500, detail="An unexpected error occurred: {}".format(e)
status_code=500,
detail="An unexpected error occurred: {}".format(e),
)


Expand Down Expand Up @@ -453,7 +454,7 @@ def get_vs_condensed_trending_data(filters, current_user):
raise HTTPException(status_code=404, detail="Organization not found.")

if (
not is_global_view_admin(current_user)
not is_global_view_admin(current_user) | is_analytics_user(current_user)
and not current_user.user_type == "regionalAdmin"
):
org_ids = get_org_memberships(current_user)
Expand Down Expand Up @@ -538,6 +539,7 @@ def get_vs_trending_data(filters, current_user):

if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not current_user.user_type == "regionalAdmin"
):
org_ids = get_org_memberships(current_user)
Expand Down
15 changes: 12 additions & 3 deletions backend/src/xfd_django/xfd_api/api_methods/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from ..auth import (
can_access_user,
is_analytics_user,
is_global_view_admin,
is_global_write_admin,
is_org_admin,
Expand Down Expand Up @@ -193,7 +194,11 @@ def get_users(current_user):
"""Retrieve a list of all users."""
try:
# Check if user is a regional admin or global admin
if not is_global_view_admin(current_user) | is_regional_admin(current_user):
if (
not is_global_view_admin(current_user)
| is_regional_admin(current_user)
| is_analytics_user(current_user)
):
raise HTTPException(status_code=401, detail="Unauthorized")

users = User.objects.all().prefetch_related("roles__organization")
Expand Down Expand Up @@ -241,7 +246,7 @@ def get_users(current_user):
def get_users_by_region_id(region_id, current_user):
"""List users with specific region_id."""
try:
if not is_regional_admin(current_user):
if not is_regional_admin(current_user) | is_analytics_user(current_user):
raise HTTPException(status_code=401, detail="Unauthorized")

if not region_id:
Expand Down Expand Up @@ -357,7 +362,11 @@ def get_users_v2(state, region_id, invite_pending, current_user):
"""Retrieve a list of users based on optional filter parameters."""
try:
# Check if user is a regional admin or global admin
if not is_regional_admin(current_user) | is_global_view_admin(current_user):
if (
not is_regional_admin(current_user)
| is_global_view_admin(current_user)
| is_analytics_user(current_user)
):
raise HTTPException(status_code=401, detail="Unauthorized")

filters = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
Vulnerability,
)

from ..auth import get_org_memberships, is_global_view_admin
from ..auth import get_org_memberships, is_analytics_user, is_global_view_admin
from ..helpers.filter_helpers import apply_vuln_filters, sort_direction
from ..helpers.s3_client import S3Client
from ..models import Domain
Expand Down Expand Up @@ -105,6 +105,7 @@ def get_vulnerability_by_scan_source_and_id(

if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not current_user.user_type == "regionalAdmin"
):
org_ids = get_org_memberships(current_user)
Expand Down Expand Up @@ -385,6 +386,7 @@ def search_vulnerabilities(vulnerability_search: VulnerabilitySearch, current_us
# Permissions check
if (
not is_global_view_admin(current_user)
and not is_analytics_user(current_user)
and not current_user.user_type == "regionalAdmin"
):
org_ids = get_org_memberships(current_user)
Expand Down
24 changes: 20 additions & 4 deletions backend/src/xfd_django/xfd_api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ def is_regional_admin(current_user) -> bool:
return current_user and current_user.user_type in ["regionalAdmin", "globalAdmin"]


def is_analytics_user(current_user) -> bool:
"""Check if the user has analytics permissions."""
return current_user and current_user.user_type in ["analytics", "globalAdmin"]


def is_org_admin(current_user, organization_id) -> bool:
"""Check if the user is an admin of the given organization."""
if not organization_id:
Expand Down Expand Up @@ -461,6 +466,7 @@ def get_stats_org_ids(current_user, filters):
or (is_regional_admin_for_organization(current_user, org_id))
or (is_org_admin(current_user, org_id))
or (get_org_memberships(current_user))
or (is_analytics_user(current_user))
):
organization_ids.add(org_id)

Expand All @@ -483,11 +489,21 @@ def get_stats_org_ids(current_user, filters):
for tag_id in tags_filter:
organizations_by_tag = get_tag_organizations(current_user, tag_id)
organization_ids.update(organizations_by_tag)

# Case 3: Regional admin
# Case 3: Analytics view
elif is_analytics_user(current_user):
# Get organizations by region
if regions_filter:
organizations_by_region = Organization.objects.filter(
region_id__in=regions_filter
).values_list("id", flat=True)
organization_ids.update(organizations_by_region)
# Get organizations by tag
for tag_id in tags_filter:
organizations_by_tag = get_tag_organizations(current_user, tag_id)
organization_ids.update(organizations_by_tag)
# Case 4: Regional admin
elif current_user.user_type in ["regionalAdmin"]:
user_region_id = current_user.region_id

# Allow only organizations in the user's region
organizations_in_region = Organization.objects.filter(
region_id=user_region_id
Expand All @@ -508,7 +524,7 @@ def get_stats_org_ids(current_user, filters):
]
organization_ids.update(regional_tag_organizations)

# Case 4: Standard user
# Case 5: Standard user
else:
# Allow only organizations where the user is a member
user_organization_ids = current_user.roles.values_list(
Expand Down
1 change: 1 addition & 0 deletions backend/src/xfd_django/xfd_api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ class Meta:
class UserType(models.TextChoices):
"""User type definition."""

ANALYTICS = "analytics"
GLOBAL_ADMIN = "globalAdmin"
GLOBAL_VIEW = "globalView"
REGIONAL_ADMIN = "regionalAdmin"
Expand Down
1 change: 1 addition & 0 deletions backend/src/xfd_django/xfd_api/schema_models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
class UserType(Enum):
"""User Type."""

ANALYTICS = "analytics"
GLOBAL_ADMIN = "globalAdmin"
GLOBAL_VIEW = "globalView"
REGIONAL_ADMIN = "regionalAdmin"
Expand Down
Loading
Loading