admin key can read all images + fix env var for cloud function

This commit is contained in:
johnpccd 2025-05-25 00:39:08 +02:00
parent f80505bd74
commit 348bcf93a7
7 changed files with 429 additions and 110 deletions

View File

@ -0,0 +1 @@
{"ID":"9b771a70-f4b7-dbb4-f919-38d27ca55e23","Operation":"OperationTypeApply","Info":"","Who":"DESKTOP\\habal@Desktop","Version":"1.10.1","Created":"2025-05-24T22:38:22.6064375Z","Path":"terraform.tfstate"}

View File

@ -66,6 +66,8 @@ resource "google_cloudfunctions2_function" "image_processor" {
# Logging
LOG_LEVEL = "INFO"
PROJECT_ID = var.project_id
}
service_account_email = local.cloud_function_service_account

View File

@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.10.1",
"serial": 410,
"serial": 415,
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
"outputs": {
"cloud_function_name": {
@ -98,16 +98,16 @@
"attributes": {
"exclude_symlink_directories": null,
"excludes": null,
"id": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
"output_base64sha256": "0wAfDV7tH41jEspQ3LBLvIEnrQ6XU2aEtK2GWsMdyCA=",
"output_base64sha512": "glsNAiHzSTOy9mGDckkSyDJhBVFDtLh8Xr6+hSxtCT8nok9qNGO+61iTRLU42OPxaPS/BrbDAXJeT86F3riefA==",
"id": "bfc4b3b9de401cd15676a09a067a8e4095b0bf4e",
"output_base64sha256": "cXx9sC1kIbTDG7BlKAtf3FUasHLZ/wZPzVoyBvt8p9Q=",
"output_base64sha512": "dcntRZ4Hz4dfBBj7YVsTzx+SEAqCXZCD8TAAh8cr5xa3uT2Lsmtf8zpxpyQEeMlsCNaF8dUohGQ7BD9LJYigPw==",
"output_file_mode": null,
"output_md5": "a5d3a7fe131c972bf8d0edf309545042",
"output_md5": "46efa1aee5386e2f244b597289c7c4ba",
"output_path": "./function-source.zip",
"output_sha": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
"output_sha256": "d3001f0d5eed1f8d6312ca50dcb04bbc8127ad0e97536684b4ad865ac31dc820",
"output_sha512": "825b0d0221f34933b2f66183724912c83261055143b4b87c5ebebe852c6d093f27a24f6a3463beeb589344b538d8e3f168f4bf06b6c301725e4fce85deb89e7c",
"output_size": 4487,
"output_sha": "bfc4b3b9de401cd15676a09a067a8e4095b0bf4e",
"output_sha256": "717c7db02d6421b4c31bb065280b5fdc551ab072d9ff064fcd5a3206fb7ca7d4",
"output_sha512": "75c9ed459e07cf875f0418fb615b13cf1f92100a825d9083f1300087c72be716b7b93d8bb26b5ff33a71a7240478c96c08d685f1d52884643b043f4b2588a03f",
"output_size": 6734,
"source": [],
"source_content": null,
"source_content_filename": null,
@ -182,7 +182,7 @@
"goog-terraform-provisioned": "true"
},
"generation": 1,
"labels": null,
"labels": {},
"namespace": "gen-lang-client-0424120530",
"resource_version": "AAY16UbSm4k",
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
@ -256,8 +256,8 @@
"container_concurrency": 80,
"containers": [
{
"args": null,
"command": null,
"args": [],
"command": [],
"env": [
{
"name": "FIRESTORE_DATABASE_NAME",
@ -332,7 +332,7 @@
"cpu": "1",
"memory": "1Gi"
},
"requests": null
"requests": {}
}
],
"startup_probe": [
@ -354,7 +354,7 @@
"working_dir": ""
}
],
"node_selector": null,
"node_selector": {},
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
"serving_state": "",
"timeout_seconds": 300,
@ -596,6 +596,13 @@
}
]
},
{
"mode": "managed",
"type": "google_compute_address",
"name": "vector_db_static_ip",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": []
},
{
"mode": "managed",
"type": "google_compute_firewall",
@ -822,7 +829,7 @@
},
{
"type": "get_attr",
"value": "disk_encryption_key_rsa"
"value": "disk_encryption_key_raw"
}
],
[
@ -839,7 +846,7 @@
},
{
"type": "get_attr",
"value": "disk_encryption_key_raw"
"value": "disk_encryption_key_rsa"
}
]
],
@ -868,8 +875,8 @@
"database_edition": "STANDARD",
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"deletion_policy": "ABANDON",
"earliest_version_time": "2025-05-24T21:29:52.924798Z",
"etag": "IPHgo4eUvY0DMKrW4vCEvY0D",
"earliest_version_time": "2025-05-24T21:38:29.341046Z",
"etag": "IOGow/2VvY0DMKrW4vCEvY0D",
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
"key_prefix": "",
"location_id": "us-central1",
@ -989,18 +996,18 @@
{
"mode": "managed",
"type": "google_project_iam_member",
"name": "function_vision",
"name": "function_vertex_ai",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"condition": [],
"etag": "BwY16LCINIE=",
"id": "gen-lang-client-0424120530/roles/ml.developer/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"etag": "BwY16WAsDU4=",
"id": "gen-lang-client-0424120530/roles/aiplatform.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com",
"project": "gen-lang-client-0424120530",
"role": "roles/ml.developer"
"role": "roles/aiplatform.user"
},
"sensitive_attributes": [],
"private": "bnVsbA==",
@ -1016,6 +1023,20 @@
"name": "services",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
{
"index_key": "aiplatform.googleapis.com",
"schema_version": 0,
"attributes": {
"disable_dependent_services": null,
"disable_on_destroy": false,
"id": "gen-lang-client-0424120530/aiplatform.googleapis.com",
"project": "gen-lang-client-0424120530",
"service": "aiplatform.googleapis.com",
"timeouts": null
},
"sensitive_attributes": [],
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
},
{
"index_key": "cloudbuild.googleapis.com",
"schema_version": 0,
@ -1493,21 +1514,21 @@
"content_encoding": "",
"content_language": "",
"content_type": "application/zip",
"crc32c": "Y4Q5hw==",
"crc32c": "kTROsA==",
"customer_encryption": [],
"detect_md5hash": "pdOn/hMclyv40O3zCVRQQg==",
"detect_md5hash": "Ru+hruU4bi8kS1lyicfEug==",
"event_based_hold": false,
"generation": 1748125796837241,
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"generation": 1748126317190781,
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"kms_key_name": "",
"md5hash": "pdOn/hMclyv40O3zCVRQQg==",
"md5hexhash": "a5d3a7fe131c972bf8d0edf309545042",
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-a5d3a7fe131c972bf8d0edf309545042.zip?generation=1748125796837241\u0026alt=media",
"md5hash": "Ru+hruU4bi8kS1lyicfEug==",
"md5hexhash": "46efa1aee5386e2f244b597289c7c4ba",
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-46efa1aee5386e2f244b597289c7c4ba.zip?generation=1748126317190781\u0026alt=media",
"metadata": null,
"name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"output_name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"output_name": "function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"retention": [],
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-46efa1aee5386e2f244b597289c7c4ba.zip",
"source": "./function-source.zip",
"storage_class": "STANDARD",
"temporary_hold": false,

View File

@ -1,7 +1,7 @@
{
"version": 4,
"terraform_version": "1.10.1",
"serial": 404,
"serial": 410,
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
"outputs": {
"cloud_function_name": {
@ -98,16 +98,16 @@
"attributes": {
"exclude_symlink_directories": null,
"excludes": null,
"id": "045029ac803155784c12f8d587fee56b85b1fbe9",
"output_base64sha256": "b/FgNMMT30JSXfrLRXNkWeNc6i22YAmT3YwQRTw1+A4=",
"output_base64sha512": "7GDDTkHwwQVAlwSxe7yzgtGccMNIRCQ7t72ZRk7bcfDI1tzpruhJ5G/0AbrUMXWQO6LffnWtwumQ7XdFHAIzBA==",
"id": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
"output_base64sha256": "0wAfDV7tH41jEspQ3LBLvIEnrQ6XU2aEtK2GWsMdyCA=",
"output_base64sha512": "glsNAiHzSTOy9mGDckkSyDJhBVFDtLh8Xr6+hSxtCT8nok9qNGO+61iTRLU42OPxaPS/BrbDAXJeT86F3riefA==",
"output_file_mode": null,
"output_md5": "34d81725abbd4f423de71ecd4215d116",
"output_md5": "a5d3a7fe131c972bf8d0edf309545042",
"output_path": "./function-source.zip",
"output_sha": "045029ac803155784c12f8d587fee56b85b1fbe9",
"output_sha256": "6ff16034c313df42525dfacb45736459e35cea2db6600993dd8c10453c35f80e",
"output_sha512": "ec60c34e41f0c105409704b17bbcb382d19c70c34844243bb7bd99464edb71f0c8d6dce9aee849e46ff401bad43175903ba2df7e75adc2e990ed77451c023304",
"output_size": 5014,
"output_sha": "7a7a706b5bba3a12744f2dd109eb18de7112f351",
"output_sha256": "d3001f0d5eed1f8d6312ca50dcb04bbc8127ad0e97536684b4ad865ac31dc820",
"output_sha512": "825b0d0221f34933b2f66183724912c83261055143b4b87c5ebebe852c6d093f27a24f6a3463beeb589344b538d8e3f168f4bf06b6c301725e4fce85deb89e7c",
"output_size": 4487,
"source": [],
"source_content": null,
"source_content_filename": null,
@ -170,11 +170,9 @@
"run.googleapis.com/ingress": "all"
},
"effective_annotations": {
"run.googleapis.com/client-name": "gcloud",
"run.googleapis.com/client-version": "431.0.0",
"run.googleapis.com/ingress": "all",
"run.googleapis.com/ingress-status": "all",
"run.googleapis.com/operation-id": "e4d7484f-39e4-4dde-8105-28d285eb927b",
"run.googleapis.com/operation-id": "7869f742-fe94-42d0-8d82-a1462681980d",
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
"serving.knative.dev/creator": "johnpccd3@gmail.com",
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
@ -183,15 +181,15 @@
"cloud.googleapis.com/location": "us-central1",
"goog-terraform-provisioned": "true"
},
"generation": 2,
"labels": {},
"generation": 1,
"labels": null,
"namespace": "gen-lang-client-0424120530",
"resource_version": "AAY16Gy+yWQ",
"resource_version": "AAY16UbSm4k",
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
"terraform_labels": {
"goog-terraform-provisioned": "true"
},
"uid": "c67276c9-0c25-4a6c-8f39-4ea942599769"
"uid": "d5532269-ab10-4b77-b90f-698306bf0919"
}
],
"name": "sereact",
@ -218,14 +216,14 @@
"type": "RoutesReady"
}
],
"latest_created_revision_name": "sereact-00002-cew",
"latest_ready_revision_name": "sereact-00002-cew",
"observed_generation": 2,
"latest_created_revision_name": "sereact-00001-9rv",
"latest_ready_revision_name": "sereact-00001-9rv",
"observed_generation": 1,
"traffic": [
{
"latest_revision": true,
"percent": 100,
"revision_name": "sereact-00002-cew",
"revision_name": "sereact-00001-9rv",
"tag": "",
"url": ""
}
@ -246,7 +244,7 @@
"labels": {
"run.googleapis.com/startupProbeType": "Default"
},
"name": "sereact-00002-cew",
"name": "",
"namespace": "",
"resource_version": "",
"self_link": "",
@ -258,8 +256,8 @@
"container_concurrency": 80,
"containers": [
{
"args": [],
"command": [],
"args": null,
"command": null,
"env": [
{
"name": "FIRESTORE_DATABASE_NAME",
@ -334,7 +332,7 @@
"cpu": "1",
"memory": "1Gi"
},
"requests": {}
"requests": null
}
],
"startup_probe": [
@ -356,7 +354,7 @@
"working_dir": ""
}
],
"node_selector": {},
"node_selector": null,
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
"serving_state": "",
"timeout_seconds": 300,
@ -437,7 +435,7 @@
"schema_version": 0,
"attributes": {
"condition": [],
"etag": "BwY16Etxb+g=",
"etag": "BwY16UdHJ00=",
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
"location": "us-central1",
"member": "allUsers",
@ -471,7 +469,7 @@
"automatic_update_policy": [
{}
],
"build": "projects/761163285547/locations/us-central1/builds/1b8e28d1-ee4d-4d2f-acf2-47e2b03aa421",
"build": "projects/761163285547/locations/us-central1/builds/b2b7e513-e00e-462a-8ac8-94abdfb4a0b9",
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
"entry_point": "process_image_embedding",
"environment_variables": {},
@ -485,7 +483,7 @@
{
"bucket": "gen-lang-client-0424120530-cloud-function-source",
"generation": 1748123369545880,
"object": "function-source-34d81725abbd4f423de71ecd4215d116.zip"
"object": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip"
}
]
}
@ -554,7 +552,7 @@
"goog-terraform-provisioned": "true"
},
"timeouts": null,
"update_time": "2025-05-24T22:08:16.899711009Z",
"update_time": "2025-05-24T22:31:52.525335119Z",
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
},
"sensitive_attributes": [
@ -598,13 +596,6 @@
}
]
},
{
"mode": "managed",
"type": "google_compute_address",
"name": "vector_db_static_ip",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": []
},
{
"mode": "managed",
"type": "google_compute_firewall",
@ -877,8 +868,8 @@
"database_edition": "STANDARD",
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"deletion_policy": "ABANDON",
"earliest_version_time": "2025-05-24T21:26:23.088753Z",
"etag": "IOqynKOTvY0DMKrW4vCEvY0D",
"earliest_version_time": "2025-05-24T21:29:52.924798Z",
"etag": "IPHgo4eUvY0DMKrW4vCEvY0D",
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
"key_prefix": "",
"location_id": "us-central1",
@ -1502,21 +1493,21 @@
"content_encoding": "",
"content_language": "",
"content_type": "application/zip",
"crc32c": "YXAlNA==",
"crc32c": "Y4Q5hw==",
"customer_encryption": [],
"detect_md5hash": "NNgXJau9T0I95x7NQhXRFg==",
"detect_md5hash": "pdOn/hMclyv40O3zCVRQQg==",
"event_based_hold": false,
"generation": 1748124439573408,
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-34d81725abbd4f423de71ecd4215d116.zip",
"generation": 1748125796837241,
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"kms_key_name": "",
"md5hash": "NNgXJau9T0I95x7NQhXRFg==",
"md5hexhash": "34d81725abbd4f423de71ecd4215d116",
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-34d81725abbd4f423de71ecd4215d116.zip?generation=1748124439573408\u0026alt=media",
"metadata": {},
"name": "function-source-34d81725abbd4f423de71ecd4215d116.zip",
"output_name": "function-source-34d81725abbd4f423de71ecd4215d116.zip",
"md5hash": "pdOn/hMclyv40O3zCVRQQg==",
"md5hexhash": "a5d3a7fe131c972bf8d0edf309545042",
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-a5d3a7fe131c972bf8d0edf309545042.zip?generation=1748125796837241\u0026alt=media",
"metadata": null,
"name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"output_name": "function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"retention": [],
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-34d81725abbd4f423de71ecd4215d116.zip",
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-a5d3a7fe131c972bf8d0edf309545042.zip",
"source": "./function-source.zip",
"storage_class": "STANDARD",
"temporary_hold": false,

View File

@ -136,10 +136,22 @@ async def list_images(
current_user: UserModel = Depends(get_current_user)
):
"""
List images for the current user's team
List images for the current user's team, or all images if user is admin.
Regular users can only see images from their own team.
Admin users can see all images across all teams.
Args:
skip: Number of records to skip for pagination
limit: Maximum number of records to return (1-100)
collection_id: Optional filter by collection ID
tags: Optional comma-separated list of tags to filter by
Returns:
List of images with pagination metadata
"""
log_request(
{"path": request.url.path, "method": request.method, "skip": skip, "limit": limit},
{"path": request.url.path, "method": request.method, "skip": skip, "limit": limit, "is_admin": current_user.is_admin},
user_id=str(current_user.id),
team_id=str(current_user.team_id)
)
@ -149,21 +161,37 @@ async def list_images(
if tags:
tag_filter = [tag.strip() for tag in tags.split(',') if tag.strip()]
# Get images
images = await image_repository.get_by_team(
current_user.team_id,
skip=skip,
limit=limit,
collection_id=ObjectId(collection_id) if collection_id else None,
tags=tag_filter
)
# Check if user is admin - if so, get all images across all teams
if current_user.is_admin:
# Admin users can see all images across all teams
images = await image_repository.get_all_with_pagination(
skip=skip,
limit=limit,
collection_id=ObjectId(collection_id) if collection_id else None,
tags=tag_filter
)
# Get total count
total = await image_repository.count_by_team(
current_user.team_id,
collection_id=ObjectId(collection_id) if collection_id else None,
tags=tag_filter
)
# Get total count for admin
total = await image_repository.count_all(
collection_id=ObjectId(collection_id) if collection_id else None,
tags=tag_filter
)
else:
# Regular users only see images from their team
images = await image_repository.get_by_team(
current_user.team_id,
skip=skip,
limit=limit,
collection_id=ObjectId(collection_id) if collection_id else None,
tags=tag_filter
)
# Get total count for regular user
total = await image_repository.count_by_team(
current_user.team_id,
collection_id=ObjectId(collection_id) if collection_id else None,
tags=tag_filter
)
# Convert to response
response_images = []
@ -211,7 +239,7 @@ async def get_image(
Get image metadata by ID
"""
log_request(
{"path": request.url.path, "method": request.method, "image_id": image_id},
{"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin},
user_id=str(current_user.id),
team_id=str(current_user.team_id)
)
@ -226,8 +254,8 @@ async def get_image(
if not image:
raise HTTPException(status_code=404, detail="Image not found")
# Check team access
if image.team_id != current_user.team_id:
# Check team access (admins can access any image)
if not current_user.is_admin and image.team_id != current_user.team_id:
raise HTTPException(status_code=403, detail="Not authorized to access this image")
# Update last accessed
@ -278,7 +306,7 @@ async def download_image(
Download image file
"""
log_request(
{"path": request.url.path, "method": request.method, "image_id": image_id},
{"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin},
user_id=str(current_user.id),
team_id=str(current_user.team_id)
)
@ -293,8 +321,8 @@ async def download_image(
if not image:
raise HTTPException(status_code=404, detail="Image not found")
# Check team access
if image.team_id != current_user.team_id:
# Check team access (admins can access any image)
if not current_user.is_admin and image.team_id != current_user.team_id:
raise HTTPException(status_code=403, detail="Not authorized to access this image")
# Get file from storage
@ -323,7 +351,7 @@ async def update_image(
Update image metadata
"""
log_request(
{"path": request.url.path, "method": request.method, "image_id": image_id},
{"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin},
user_id=str(current_user.id),
team_id=str(current_user.team_id)
)
@ -338,8 +366,8 @@ async def update_image(
if not image:
raise HTTPException(status_code=404, detail="Image not found")
# Check team access
if image.team_id != current_user.team_id:
# Check team access (admins can update any image)
if not current_user.is_admin and image.team_id != current_user.team_id:
raise HTTPException(status_code=403, detail="Not authorized to update this image")
# Update image
@ -398,7 +426,7 @@ async def delete_image(
Delete an image
"""
log_request(
{"path": request.url.path, "method": request.method, "image_id": image_id},
{"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin},
user_id=str(current_user.id),
team_id=str(current_user.team_id)
)
@ -413,8 +441,8 @@ async def delete_image(
if not image:
raise HTTPException(status_code=404, detail="Image not found")
# Check team access
if image.team_id != current_user.team_id:
# Check team access (admins can delete any image)
if not current_user.is_admin and image.team_id != current_user.team_id:
raise HTTPException(status_code=403, detail="Not authorized to delete this image")
# Delete from storage

View File

@ -173,5 +173,80 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
logger.error(f"Error getting images by tag: {e}")
raise
async def get_all_with_pagination(
self,
skip: int = 0,
limit: int = 50,
collection_id: Optional[ObjectId] = None,
tags: Optional[List[str]] = None
) -> List[ImageModel]:
"""
Get all images across all teams with pagination and filtering (admin only)
Args:
skip: Number of records to skip
limit: Maximum number of records to return
collection_id: Optional collection ID filter
tags: Optional list of tags to filter by
Returns:
List of images
"""
try:
# Get all images
images = await self.get_all()
# Filter by collection if specified
if collection_id:
images = [image for image in images if image.collection_id == collection_id]
# Filter by tags if specified
if tags:
images = [
image for image in images
if any(tag in image.tags for tag in tags)
]
# Apply pagination
return images[skip:skip + limit]
except Exception as e:
logger.error(f"Error getting all images with pagination: {e}")
raise
async def count_all(
self,
collection_id: Optional[ObjectId] = None,
tags: Optional[List[str]] = None
) -> int:
"""
Count all images across all teams with filtering (admin only)
Args:
collection_id: Optional collection ID filter
tags: Optional list of tags to filter by
Returns:
Count of images
"""
try:
# Get all images
images = await self.get_all()
# Filter by collection if specified
if collection_id:
images = [image for image in images if image.collection_id == collection_id]
# Filter by tags if specified
if tags:
images = [
image for image in images
if any(tag in image.tags for tag in tags)
]
return len(images)
except Exception as e:
logger.error(f"Error counting all images: {e}")
raise
# Create a singleton repository
firestore_image_repository = FirestoreImageRepository()

201
test_admin_images.py Normal file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Test script to verify admin image access functionality.
This script tests that:
1. Regular users can only see images from their own team
2. Admin users can see all images across all teams
"""
import asyncio
import sys
import os
from datetime import datetime
from bson import ObjectId
# Add the src directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from src.models.image import ImageModel
from src.models.user import UserModel
from src.db.repositories.image_repository import image_repository
from src.db.repositories.user_repository import user_repository
from src.db.providers.firestore_provider import firestore_db
async def setup_test_data():
"""Set up test data for the admin functionality test"""
print("Setting up test data...")
# Create two teams
team1_id = ObjectId()
team2_id = ObjectId()
# Create users
regular_user = UserModel(
email="regular@test.com",
name="Regular User",
team_id=team1_id,
is_admin=False
)
admin_user = UserModel(
email="admin@test.com",
name="Admin User",
team_id=team1_id,
is_admin=True
)
# Create test images for team 1
image1_team1 = ImageModel(
filename="team1-image1.jpg",
original_filename="team1_image1.jpg",
file_size=1024,
content_type="image/jpeg",
storage_path="images/team1-image1.jpg",
team_id=team1_id,
uploader_id=regular_user.id,
description="Team 1 Image 1",
tags=["team1", "test"]
)
image2_team1 = ImageModel(
filename="team1-image2.jpg",
original_filename="team1_image2.jpg",
file_size=2048,
content_type="image/jpeg",
storage_path="images/team1-image2.jpg",
team_id=team1_id,
uploader_id=admin_user.id,
description="Team 1 Image 2",
tags=["team1", "admin"]
)
# Create test images for team 2
image1_team2 = ImageModel(
filename="team2-image1.jpg",
original_filename="team2_image1.jpg",
file_size=1536,
content_type="image/jpeg",
storage_path="images/team2-image1.jpg",
team_id=team2_id,
uploader_id=ObjectId(), # Different user from team 2
description="Team 2 Image 1",
tags=["team2", "test"]
)
return {
'regular_user': regular_user,
'admin_user': admin_user,
'team1_id': team1_id,
'team2_id': team2_id,
'images': [image1_team1, image2_team1, image1_team2]
}
async def test_regular_user_access(regular_user, team1_id):
"""Test that regular users only see their team's images"""
print("\n=== Testing Regular User Access ===")
# Simulate getting images for regular user (team-filtered)
team1_images = await image_repository.get_by_team(team1_id, skip=0, limit=50)
team1_count = await image_repository.count_by_team(team1_id)
print(f"Regular user sees {len(team1_images)} images from their team")
print(f"Total count for team: {team1_count}")
for image in team1_images:
print(f" - {image.filename} (Team: {image.team_id})")
# Verify all images belong to the user's team
for image in team1_images:
assert image.team_id == team1_id, f"Regular user should not see image from different team: {image.filename}"
print("✅ Regular user access test passed - only sees team images")
return len(team1_images)
async def test_admin_user_access(admin_user):
"""Test that admin users see all images across all teams"""
print("\n=== Testing Admin User Access ===")
# Simulate getting all images for admin user
all_images = await image_repository.get_all_with_pagination(skip=0, limit=50)
all_count = await image_repository.count_all()
print(f"Admin user sees {len(all_images)} images across all teams")
print(f"Total count across all teams: {all_count}")
teams_seen = set()
for image in all_images:
teams_seen.add(str(image.team_id))
print(f" - {image.filename} (Team: {image.team_id})")
print(f"Admin sees images from {len(teams_seen)} different teams")
# Verify admin sees more images than regular user would
assert len(all_images) >= 2, "Admin should see images from multiple teams"
assert len(teams_seen) >= 2, "Admin should see images from at least 2 teams"
print("✅ Admin user access test passed - sees all images across teams")
return len(all_images)
async def main():
"""Main test function"""
print("🧪 Testing Admin Image Access Functionality")
print("=" * 50)
try:
# Connect to database
firestore_db.connect()
print("✅ Connected to Firestore")
# Set up test data
test_data = await setup_test_data()
# Create test images in database
created_images = []
for image in test_data['images']:
created_image = await image_repository.create(image)
created_images.append(created_image)
print(f"Created test image: {created_image.filename}")
# Test regular user access
regular_count = await test_regular_user_access(
test_data['regular_user'],
test_data['team1_id']
)
# Test admin user access
admin_count = await test_admin_user_access(test_data['admin_user'])
# Verify admin sees more images than regular user
print(f"\n=== Summary ===")
print(f"Regular user images: {regular_count}")
print(f"Admin user images: {admin_count}")
if admin_count > regular_count:
print("✅ SUCCESS: Admin sees more images than regular user")
else:
print("❌ FAILURE: Admin should see more images than regular user")
# Clean up test data
print(f"\n=== Cleanup ===")
for image in created_images:
await image_repository.delete(image.id)
print(f"Deleted test image: {image.filename}")
print("✅ Test completed successfully!")
except Exception as e:
print(f"❌ Test failed with error: {e}")
import traceback
traceback.print_exc()
finally:
# Disconnect from database
firestore_db.disconnect()
print("✅ Disconnected from Firestore")
if __name__ == "__main__":
asyncio.run(main())