Aplicar lógica de validación de tenant y compañía a todos los endpoints relevantes en los módulos: client_and_provider, classes, country_rule_oct, exchange_rate, fraction_rule_octave, package, parts, permission_rule_oct, seal

This commit is contained in:
2025-11-09 18:13:08 -06:00
parent 9b01632fde
commit 870bc36590
21 changed files with 689 additions and 2 deletions

View File

@@ -35,6 +35,13 @@ async def list_classes(
"""
List classes with optional filters and pagination
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClassService(db)
search_params = ClassSearchDTO(
client_id=client_id,
@@ -58,6 +65,13 @@ async def get_classes_by_client(
"""
Get all classes for a specific client
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClassService(db)
return service.search_by_client(client_id, skip, limit)

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_classes(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/classes/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_class_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/classes/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_class_forbidden():
response = client.post("/classes/", json={"name": "Test Class"})
assert response.status_code in (403, 405, 404)
def test_update_class_forbidden():
response = client.put("/classes/1", json={"name": "Updated Class"})
assert response.status_code in (403, 405, 404)

View File

@@ -28,6 +28,17 @@ async def create_client_provider(
"""
Create a new client or provider in the system
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
# Ensure the client_data is associated with the correct tenant and company
if client_data.tenant_id != tenant_id or client_data.company_id != company_id:
raise HTTPException(status_code=400, detail="Mismatch in tenant or company association")
service = ClientProviderService(db)
return service.create_client_provider(client_data)
@@ -45,6 +56,13 @@ async def list_clients_providers(
"""
List clients and providers with optional filters and pagination
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
return service.list_clients_providers(skip, limit, search, client_or_provider, enabled_only)
@@ -59,6 +77,13 @@ async def get_clients_only(
"""
Get only clients (client_or_provider = 'C')
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
return service.get_clients_only(skip, limit)
@@ -73,6 +98,13 @@ async def get_providers_only(
"""
Get only providers (client_or_provider = 'P')
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
return service.get_providers_only(skip, limit)
@@ -86,6 +118,13 @@ async def search_by_rfc(
"""
Search clients/providers by RFC
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
return service.search_by_rfc(rfc)
@@ -99,6 +138,13 @@ async def get_client_provider(
"""
Get client/provider by ID with all related information
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:
@@ -116,6 +162,13 @@ async def update_client_provider(
"""
Update client/provider information
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
client = service.update_client_provider(client_id, client_data)
if not client:
@@ -134,6 +187,13 @@ async def delete_client_provider(
Note: This will completely remove the client/provider and all related data.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
if not service.delete_client_provider(client_id):
raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found")
@@ -148,6 +208,13 @@ async def toggle_client_provider_status(
"""
Toggle client/provider enabled/disabled status
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
client = service.toggle_status(client_id)
if not client:
@@ -165,6 +232,13 @@ async def get_client_provider_address(
"""
Get only address information for a client/provider
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:
@@ -185,6 +259,13 @@ async def get_client_provider_programs(
"""
Get only programs information for a client/provider
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:
@@ -205,6 +286,13 @@ async def get_client_provider_basic_info(
"""
Get basic information for a client/provider (without address and programs)
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
if not client:

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_clients_and_providers(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/client_and_provider/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_client_or_provider_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/client_and_provider/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_client_or_provider_forbidden():
response = client.post("/client_and_provider/", json={"name": "Test Client/Provider"})
assert response.status_code in (403, 405, 404)
def test_update_client_or_provider_forbidden():
response = client.put("/client_and_provider/1", json={"name": "Updated Client/Provider"})
assert response.status_code in (403, 405, 404)

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_companies(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/company/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_company_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/company/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_company_forbidden():
response = client.post("/company/", json={"name": "Test Company"})
assert response.status_code in (403, 405, 404)
def test_update_company_forbidden():
response = client.put("/company/1", json={"name": "Updated Company"})
assert response.status_code in (403, 405, 404)

View File

@@ -18,6 +18,13 @@ async def list_countries(
"""
List all CountryRuleOct entries.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return db.query(CountryRuleOctService).all()
@@ -33,6 +40,13 @@ async def read_country_rule(
"""
Get a specific CountryRuleOct by its composite key.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
country = CountryRuleOctService.get_country_by_keys(db, permission, line, fraction, country_code)
if not country:
raise HTTPException(status_code=404, detail="CountryRuleOct not found")

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_country_rules(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/country-rule-oct/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_country_rule_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/country-rule-oct/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_country_rule_forbidden():
response = client.post("/country-rule-oct/", json={"rule": "Test Rule"})
assert response.status_code in (403, 405, 404)
def test_update_country_rule_forbidden():
response = client.put("/country-rule-oct/1", json={"rule": "Updated Rule"})
assert response.status_code in (403, 405, 404)

View File

@@ -18,6 +18,13 @@ async def list_exchange_rates(
"""
List all ExchangeRate entries.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return db.query(ExchangeRateService).all()
@@ -30,6 +37,13 @@ async def read_exchange_rate(
"""
Get a specific ExchangeRate by its date.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
exchange_rate = ExchangeRateService.get_exchange_rate_by_date(db, date)
if not exchange_rate:
raise HTTPException(status_code=404, detail="ExchangeRate not found")
@@ -45,6 +59,13 @@ async def create_exchange_rate(
"""
Create a new ExchangeRate entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return ExchangeRateService.create_exchange_rate(db, exchange_rate_data)
@@ -57,6 +78,13 @@ async def delete_exchange_rate(
"""
Delete an ExchangeRate by its date.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
exchange_rate = ExchangeRateService.delete_exchange_rate(db, date)
if not exchange_rate:
raise HTTPException(status_code=404, detail="ExchangeRate not found")

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_exchange_rates(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/exchange-rate/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_exchange_rate_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/exchange-rate/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_exchange_rate_forbidden():
response = client.post("/exchange-rate/", json={"rate": 1.23})
assert response.status_code in (403, 405, 404)
def test_update_exchange_rate_forbidden():
response = client.put("/exchange-rate/1", json={"rate": 1.45})
assert response.status_code in (403, 405, 404)

View File

@@ -18,6 +18,13 @@ async def list_fractions(
"""
List all FractionRuleOctave entries.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return db.query(FractionRuleOctaveService).all()
@@ -32,6 +39,13 @@ async def read_fraction(
"""
Get a specific FractionRuleOctave by its composite key.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
frac = FractionRuleOctaveService.get_fraction_by_permission_line(db, permission, line, fraction)
if not frac:
raise HTTPException(status_code=404, detail="FractionRuleOctave not found")
@@ -47,6 +61,13 @@ async def create_frac(
"""
Create a new FractionRuleOctave entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return FractionRuleOctaveService.create_frac(db, frac_data)
@@ -61,6 +82,13 @@ async def delete_fraction(
"""
Delete a FractionRuleOctave by its composite key.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
frac = FractionRuleOctaveService.delete_fraction(db, permission, line, fraction)
if not frac:
raise HTTPException(status_code=404, detail="FractionRuleOctave not found")

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_fraction_rules(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/fraction_rule_octave/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_fraction_rule_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/fraction_rule_octave/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_fraction_rule_forbidden():
response = client.post("/fraction_rule_octave/", json={"rule": "Test Rule"})
assert response.status_code in (403, 405, 404)
def test_update_fraction_rule_forbidden():
response = client.put("/fraction_rule_octave/1", json={"rule": "Updated Rule"})
assert response.status_code in (403, 405, 404)

View File

@@ -21,6 +21,13 @@ async def list_bultos(
"""
List all GBultos with pagination.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return db.query(Package).offset(skip).limit(limit).all()
@@ -33,6 +40,13 @@ async def read_bulto(
"""
Get a specific Package by its CODE.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
bulto = GBultoService.get_bulto_by_code(db, code)
if not bulto:
raise HTTPException(status_code=404, detail="Package not found")
@@ -48,6 +62,13 @@ async def create_gbulto(
"""
Create a new Package.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return GBultoService.create_gbulto(db, bulto_data)
@@ -61,6 +82,13 @@ async def update_bulto(
"""
Update an existing Package.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
bulto = GBultoService.update_bulto(db, code, bulto_data)
if not bulto:
raise HTTPException(status_code=404, detail="Package not found")
@@ -76,6 +104,13 @@ async def delete_bulto(
"""
Delete a Package by its CODE.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
bulto = GBultoService.delete_bulto(db, code)
if not bulto:
raise HTTPException(status_code=404, detail="Package not found")

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_packages(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/bultos/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_package_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/bultos/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_package_forbidden():
response = client.post("/bultos/", json={"name": "Test Package"})
assert response.status_code in (403, 405, 404)
def test_update_package_forbidden():
response = client.put("/bultos/1", json={"name": "Updated Package"})
assert response.status_code in (403, 405, 404)

View File

@@ -29,6 +29,13 @@ async def create_part(
"""
Create a new part in the system
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
return service.create_part(part_data)
@@ -49,6 +56,13 @@ async def list_parts(
"""
List parts with optional filters and pagination
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
search_params = PartSearchDTO(
client_id=client_id,
@@ -72,6 +86,13 @@ async def get_parts_by_client(
"""
Get all parts for a specific client
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
return service.search_by_client(client_id, skip, limit)
@@ -85,6 +106,13 @@ async def search_by_fraction(
"""
Search parts by tariff fraction
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
return service.search_by_fraction(fraction)
@@ -98,6 +126,13 @@ async def search_by_supplier(
"""
Search parts by supplier
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
return service.search_by_supplier(supplier)
@@ -111,6 +146,13 @@ async def get_parts_by_country(
"""
Get parts by country of origin
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
return service.get_parts_by_country(country_code)
@@ -123,6 +165,13 @@ async def get_parts_statistics(
"""
Get basic parts statistics
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
return service.get_parts_statistics()
@@ -137,6 +186,13 @@ async def get_part(
"""
Get part by composite key (client_id + part_number)
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
part = service.get_part(client_id, part_number)
if not part:
@@ -158,6 +214,13 @@ async def update_part(
"""
Update part information
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
part = service.update_part(client_id, part_number, part_data)
if not part:
@@ -180,6 +243,13 @@ async def delete_part(
Note: This will completely remove the part from the system.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
if not service.delete_part(client_id, part_number):
raise HTTPException(
@@ -198,6 +268,13 @@ async def toggle_part_status(
"""
Toggle part enabled/disabled status
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
part = service.toggle_status(client_id, part_number)
if not part:
@@ -219,6 +296,13 @@ async def get_part_basic_info(
"""
Get basic information for a part
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
part = service.get_part(client_id, part_number)
if not part:
@@ -249,6 +333,13 @@ async def get_part_regulatory_info(
"""
Get regulatory information for a part (FDA, FCC, ECCN, etc.)
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
service = PartService(db)
part = service.get_part(client_id, part_number)
if not part:

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_parts(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/parts/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_part_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/parts/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_part_forbidden():
response = client.post("/parts/", json={"name": "Test Part"})
assert response.status_code in (403, 405, 404)
def test_update_part_forbidden():
response = client.put("/parts/1", json={"name": "Updated Part"})
assert response.status_code in (403, 405, 404)

View File

@@ -18,6 +18,13 @@ async def list_permissions(
"""
List all PermissionRuleOct entries.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return db.query(PermissionRuleOctService).all()
@@ -30,6 +37,13 @@ async def read_permission(
"""
Get a specific PermissionRuleOct by its permission.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
permission = PermissionRuleOctService.get_permission_by_id(db, permission)
if not permission:
raise HTTPException(status_code=404, detail="PermissionRuleOct not found")
@@ -45,6 +59,13 @@ async def create_permission(
"""
Create a new PermissionRuleOct entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return PermissionRuleOctService.create_permission(db, permission_data)
@@ -57,6 +78,13 @@ async def delete_permission(
"""
Delete a PermissionRuleOct by its permission.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
permission = PermissionRuleOctService.delete_permission(db, permission)
if not permission:
raise HTTPException(status_code=404, detail="PermissionRuleOct not found")

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_permission_rules(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/permission_rule_oct/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_permission_rule_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/permission_rule_oct/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_permission_rule_forbidden():
response = client.post("/permission_rule_oct/", json={"rule": "Test Rule"})
assert response.status_code in (403, 405, 404)
def test_update_permission_rule_forbidden():
response = client.put("/permission_rule_oct/1", json={"rule": "Updated Rule"})
assert response.status_code in (403, 405, 404)

View File

@@ -22,6 +22,13 @@ async def list_seals(
"""
List all Seal entries.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return db.query(SealService).all()
@@ -34,6 +41,13 @@ async def read_seal(
"""
Get a specific Seal by its seal.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
seal = SealService.get_seal_by_id(db, seal)
if not seal:
raise HTTPException(status_code=404, detail="Seal not found")
@@ -49,6 +63,13 @@ async def create_seal(
"""
Create a new Seal entry.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
return SealService.create_seal(db, seal_data)
@@ -61,6 +82,13 @@ async def delete_seal(
"""
Delete a Seal by its seal.
"""
# Validate access to the tenant and company
tenant_id = current_user.get("tenant_id")
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
seal = SealService.delete_seal(db, seal)
if not seal:
raise HTTPException(status_code=404, detail="Seal not found")

View File

@@ -0,0 +1,31 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_seals(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/seal/", headers=headers)
assert response.status_code == 200
assert "items" in response.json()
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_seal_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/seal/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_seal_forbidden():
response = client.post("/seal/", json={"name": "Test Seal"})
assert response.status_code in (403, 405, 404)
def test_update_seal_forbidden():
response = client.put("/seal/1", json={"name": "Updated Seal"})
assert response.status_code in (403, 405, 404)

View File

@@ -1,7 +1,7 @@
"""
Modelo de relación entre usuarios (Keycloak) y tenants
"""
from sqlalchemy import Integer, String, DateTime, Boolean, UniqueConstraint, ForeignKeyConstraint
from sqlalchemy import Integer, String, DateTime, Boolean, UniqueConstraint, ForeignKeyConstraint, ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
@@ -34,6 +34,9 @@ class UserTenant(Base):
# ID del tenant
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
# ID de la empresa asociada
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.companies.id"), nullable=False, index=True)
# Estado de la relación
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)

View File

@@ -8,6 +8,9 @@ from jose import jwt, JWTError
from typing import Optional, Dict, Any
from .config import settings
import logging
from sqlalchemy.orm import Session
from core.database import get_core_db
from api.v1.modules.a76.user_tenant.models import UserTenant
logger = logging.getLogger(__name__)
@@ -75,16 +78,33 @@ def verify_token(token: str) -> Dict[str, Any]:
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security)
credentials: HTTPAuthorizationCredentials = Security(security),
db: Session = Depends(get_core_db)
) -> Dict[str, Any]:
"""
Dependency para obtener el usuario actual desde el token JWT
Enriquecido con tenant_id y company_id desde la tabla user_tenant
Uso en FastAPI:
current_user: dict = Depends(get_current_user)
"""
token = credentials.credentials
user_info = verify_token(token)
# Obtener user_id desde el token
user_id = user_info.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found in token")
# Consultar la tabla user_tenant para obtener tenant_id y company_id
user_tenant = db.query(UserTenant).filter(UserTenant.keycloak_user_id == user_id).first()
if not user_tenant:
raise HTTPException(status_code=403, detail="User does not have access to any tenant or company")
# Enriquecer user_info con tenant_id y company_id
user_info["tenant_id"] = user_tenant.tenant_id
user_info["company_id"] = user_tenant.company_id
return user_info