@cryptotaxi247 / CoPilot / commits / eb2054bb

Enable mimecast (#127)

* Migrate integration auth key lookups to utils for universal use * delete customer integrations from db fix * updated dependencies * add customer integrations meta table * Add customer integration metadata endpoints * Add integration meta entry for Mimecast * added mimecast deploy * Update customer integration table to deployed for Mimecast provisioning * Update integration service name to Mimecast * Remove unused code for Mimecast integration * Remove unused imports in agent_sync.py * Update scheduler login * Add logging statement stating job has started * Add hash to output file names in build configuration * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Feb 2, 2024 at 10:56 UTC eb2054bb4d85f077c45e7bd44dd564cf1aae49c6
19 files changed +464 -145
.gitignore
+1
@@ -21,6 +21,7 @@ coverage
21 # Editor directories and files
22 .vscode/*
23 !.vscode/extensions.json
24 +!.vscode/settings.json
25 .idea
26 *.suo
27 *.ntvs*
.vscode/extensions.json
+3 -1
@@ -12,6 +12,8 @@
12 "dbaeumer.vscode-eslint",
13 "Vue.volar",
14 "Gruntfuggly.todo-tree",
15 - "Vue.vscode-typescript-vue-plugin"
15 + "Vue.vscode-typescript-vue-plugin",
16 + "usernamehw.errorlens",
17 + "streetsidesoftware.code-spell-checker"
18 ]
19 }
.vscode/settings.json new
+15
@@ -0,0 +1,15 @@
1 +{
2 + "cSpell.words": [
3 + "ajoelp",
4 + "colord",
5 + "echarts",
6 + "mimecast",
7 + "picocolors",
8 + "redoc",
9 + "rushstack",
10 + "taze",
11 + "uvicorn",
12 + "venv",
13 + "Wazuh"
14 + ]
15 +}
backend/app/integrations/mimecast/checkpoint/mimecast_00002.checkpoint
+1 -1
@@ -1 +1 @@
1 -eNo9js1SwjAURt_lrjtjSolpmHHRCuqUH0GqDoybEm7SaGmwCcXG8d2tLlh_55z5vsGiODWo9zCCl1Qvn-pEEWk-dZ5uku2jyJbGvxZu3qlMCrJatNPp-e3Kefa-cdtCf3VjlpYya0WYzrlY51pU1U7tBscZ8mjG_b6Ui3hypz_uJ9fK-gccn3Ovy9UNBGCktOhgFAZgNR4qo_5ucDrkNIoiGoBosHCY6wP2ECOUU07iQczii-C6Y7-RAFpsrDb1f6wQwpzqPgy3z-skDFlCCfz8AnB4S9w
1 +eNotjs1ugkAYRd_l25bFID-DJl0MVAy0TVNBwe5w-OTXAYdRi6bvXmy6Puee3DsMyM8SqxwW0O9JewvxLU9K55Sno--PW_MS2U-xW_KG0Znz_d7UL-FuvczTRrabaCzaOugDq1SnbU4FS6-OK17DeL3KvoLVfre0P7yr6SW8-rz1ykUpWBHUfmI-gwbd4TCggoWuwVDhse2Kx4u55RjEMueOBlxipjCujjhJlNgzgxLdNB7of6DGfmJEgwvKoerEXyzjvDuLKQzeJmK6TplF4OcXmjpKpA
backend/app/integrations/mimecast/routes/mimecast.py
+14 -64
@@ -1,8 +1,5 @@
1 -from typing import Dict
2 -
1 from fastapi import APIRouter
2 from fastapi import Depends
5 -from fastapi import HTTPException
3 from fastapi import Security
4 from loguru import logger
5 from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,66 +13,12 @@ from app.integrations.mimecast.schema.mimecast import MimecastTTPURLSRequest
13 from app.integrations.mimecast.services.mimecast import get_ttp_urls
14 from app.integrations.mimecast.services.mimecast import invoke_mimecast
15 from app.integrations.routes import find_customer_integration
19 -from app.integrations.routes import get_customer_integrations_by_customer_code
20 -from app.integrations.schema import CustomerIntegrations
21 -from app.integrations.schema import CustomerIntegrationsResponse
16 +from app.integrations.utils.utils import extract_mimecast_auth_keys
17 +from app.integrations.utils.utils import get_customer_integration_response
18
19 integration_mimecast_router = APIRouter()
20
21
26 -async def get_customer_integration_response(customer_code: str, session: AsyncSession) -> CustomerIntegrationsResponse:
27 - """
28 - Retrieves the integration response for a customer.
29 -
30 - Args:
31 - customer_code (str): The code of the customer.
32 - session (AsyncSession): The async session object for database operations.
33 -
34 - Returns:
35 - CustomerIntegrationsResponse: The integration response for the customer.
36 -
37 - Raises:
38 - HTTPException: If the customer integration settings are not found.
39 - """
40 - customer_integration_response = await get_customer_integrations_by_customer_code(customer_code, session)
41 - if customer_integration_response.available_integrations == []:
42 - raise HTTPException(status_code=404, detail="Customer integration settings not found.")
43 - return customer_integration_response
44 -
45 -
46 -def extract_mimecast_auth_keys(customer_integration: CustomerIntegrations) -> Dict[str, str]:
47 - """
48 - Extracts the authentication keys for Office365 integration from the given customer integration.
49 -
50 - Args:
51 - customer_integration (CustomerIntegrations): The customer integration object.
52 -
53 - Returns:
54 - Dict[str, str]: A dictionary containing the authentication keys for Office365 integration.
55 -
56 - Raises:
57 - HTTPException: If no authentication keys are found for Office365 integration.
58 - """
59 - mimecast_auth_keys = {}
60 - try:
61 - for subscription in customer_integration.integration_subscriptions:
62 - if subscription.integration_service.service_name == "Mimecast":
63 - for auth_key in subscription.integration_auth_keys:
64 - mimecast_auth_keys[auth_key.auth_key_name] = auth_key.auth_value
65 - if not mimecast_auth_keys:
66 - raise HTTPException(
67 - status_code=404,
68 - detail="No auth keys found for Mimecast integration. Please create auth keys for Mimecast integration.",
69 - )
70 - except Exception as e:
71 - logger.error(f"Error extracting auth keys for Mimecast integration: {e}")
72 - raise HTTPException(
73 - status_code=404,
74 - detail="No auth keys found for Mimecast integration. Please create auth keys for Mimecast integration.",
75 - )
76 - return mimecast_auth_keys
77 -
78 -
22 @integration_mimecast_router.post(
23 "/invoke",
24 response_model=MimecastResponse,
@@ -84,14 +27,21 @@ def extract_mimecast_auth_keys(customer_integration: CustomerIntegrations) -> Di
27 )
28 async def invoke_mimecast_route(mimecast_request: MimecastRequest, session: AsyncSession = Depends(get_db)) -> MimecastResponse:
29 """
87 - Provisions Office365 integration for a customer.
30 + Invoke a mimecast integration.
31 +
32 + This function is responsible for handling the POST request to invoke a mimecast integration.
33 + It requires the following parameters:
34 + - mimecast_request: The request payload containing the necessary information for the integration.
35 + - session: The database session used for querying customer integration data.
36
89 - Args:
90 - provision_office365_request (ProvisionOffice365Request): The request object containing the necessary information for provisioning.
91 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
37 + It performs the following steps:
38 + 1. Retrieves the customer integration response from the database based on the customer code.
39 + 2. Finds the specific customer integration based on the customer code and integration name.
40 + 3. Extracts the mimecast authentication keys from the customer integration.
41 + 4. Invokes the mimecast integration using the provided request payload and authentication keys.
42
43 Returns:
94 - ProvisionOffice365Response: The response object containing the result of the provisioning.
44 + - MimecastResponse: The response model containing the result of the mimecast integration invocation.
45 """
46 customer_integration_response = await get_customer_integration_response(mimecast_request.customer_code, session)
47
backend/app/integrations/mimecast/routes/provision.py
+13 -3
@@ -7,6 +7,7 @@ from app.integrations.mimecast.schema.mimecast import MimecastScheduledResponse
7 from app.integrations.mimecast.schema.provision import ProvisionMimecastRequest
8 from app.integrations.mimecast.schema.provision import ProvisionMimecastResponse
9 from app.integrations.mimecast.services.provision import provision_mimecast
10 +from app.integrations.utils.utils import get_customer_integration_response
11 from app.schedulers.models.scheduler import CreateSchedulerRequest
12 from app.schedulers.scheduler import add_scheduler_jobs
13
@@ -23,15 +24,17 @@ async def provision_mimecast_route(
24 session: AsyncSession = Depends(get_db),
25 ) -> ProvisionMimecastResponse:
26 """
26 - Provisions Office365 integration for a customer.
27 + Provisions a mimecast integration.
28
29 Args:
29 - provision_office365_request (ProvisionOffice365Request): The request object containing the necessary information for provisioning.
30 + provision_mimecast_request (ProvisionMimecastRequest): The request object containing the necessary data for provisioning.
31 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
32
33 Returns:
33 - ProvisionOffice365Response: The response object containing the result of the provisioning.
34 + ProvisionMimecastResponse: The response object indicating the success or failure of the provisioning process.
35 """
36 + # Check if the customer integration settings are available and can be provisioned
37 + await get_customer_integration_response(provision_mimecast_request.customer_code, session)
38 await provision_mimecast(provision_mimecast_request, session)
39 await add_scheduler_jobs(
40 CreateSchedulerRequest(
@@ -40,6 +43,13 @@ async def provision_mimecast_route(
43 job_id="invoke_mimecast_integration_ttp",
44 ),
45 )
46 + await add_scheduler_jobs(
47 + CreateSchedulerRequest(
48 + function_name="invoke_mimecast_integration",
49 + time_interval=5,
50 + job_id="invoke_mimecast_integration",
51 + ),
52 + )
53 return ProvisionMimecastResponse(success=True, message="Mimecast integration provisioned.")
54
55
backend/app/integrations/mimecast/services/provision.py
+60
@@ -2,6 +2,8 @@ import json
2 from datetime import datetime
3
4 from loguru import logger
5 +from sqlalchemy import and_
6 +from sqlalchemy import update
7 from sqlalchemy.ext.asyncio import AsyncSession
8
9 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
@@ -22,6 +24,9 @@ from app.customers.routes.customers import get_customer_meta
24 from app.integrations.mimecast.schema.provision import MimecastEventStream
25 from app.integrations.mimecast.schema.provision import ProvisionMimecastRequest
26 from app.integrations.mimecast.schema.provision import ProvisionMimecastResponse
27 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
28 +from app.integrations.routes import create_integration_meta
29 +from app.integrations.schema import CustomerIntegrationsMetaSchema
30 from app.utils import get_connector_attribute
31
32
@@ -269,5 +274,60 @@ async def provision_mimecast(provision_mimecast_request: ProvisionMimecastReques
274 datasourceUid=mimecast_datasource_uid,
275 ),
276 )
277 + await create_integration_meta_entry(
278 + CustomerIntegrationsMetaSchema(
279 + customer_code=provision_mimecast_request.customer_code,
280 + integration_name="Mimecast",
281 + graylog_input_id=None,
282 + graylog_index_id=index_set_id,
283 + graylog_stream_id=stream_id,
284 + grafana_org_id=(
285 + await get_customer_meta(provision_mimecast_request.customer_code, session)
286 + ).customer_meta.customer_meta_grafana_org_id,
287 + grafana_dashboard_folder_id=grafana_mimecast_folder_id,
288 + ),
289 + session,
290 + )
291 + await update_customer_integration_table(provision_mimecast_request.customer_code, session)
292
293 return ProvisionMimecastResponse(success=True, message="Mimecast integration provisioned.")
294 +
295 +
296 +############## ! WRITE TO DB ! ##############
297 +async def create_integration_meta_entry(
298 + customer_integration_meta: CustomerIntegrationsMetaSchema,
299 + session: AsyncSession,
300 +) -> None:
301 + """
302 + Creates an entry for the customer integration meta in the database.
303 +
304 + Args:
305 + customer_integration_meta (CustomerIntegrationsMetaSchema): The customer integration meta object.
306 + session (AsyncSession): The async session object for database operations.
307 + """
308 + await create_integration_meta(customer_integration_meta, session)
309 + logger.info(f"Integration meta entry created for customer {customer_integration_meta.customer_code}.")
310 +
311 +
312 +async def update_customer_integration_table(customer_code: str, session: AsyncSession) -> None:
313 + """
314 + Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code`
315 + matches the given customer code and the `integration_service_name` is "Mimecast".
316 +
317 + Args:
318 + customer_code (str): The customer code.
319 + session (AsyncSession): The async session object for making HTTP requests.
320 + """
321 + await session.execute(
322 + update(CustomerIntegrations)
323 + .where(
324 + and_(
325 + CustomerIntegrations.customer_code == customer_code,
326 + CustomerIntegrations.integration_service_name == "Mimecast",
327 + ),
328 + )
329 + .values(deployed=True),
330 + )
331 + await session.commit()
332 +
333 + return None
backend/app/integrations/models/customer_integration_settings.py
+12
@@ -78,3 +78,15 @@ class IntegrationAuthKeys(SQLModel, table=True):
78 auth_value: str = Field(max_length=1024) # e.g., JSON/encrypted credentials
79 # Relationships
80 integration_subscription: "IntegrationSubscription" = Relationship(back_populates="integration_auth_keys") # Adjusted relationship
81 +
82 +
83 +class CustomerIntegrationsMeta(SQLModel, table=True):
84 + __tablename__ = "customer_integrations_meta"
85 + id: Optional[int] = Field(default=None, primary_key=True)
86 + customer_code: str = Field(max_length=50, nullable=False)
87 + integration_name: str = Field(max_length=255, nullable=False)
88 + graylog_input_id: Optional[str] = Field(max_length=1024)
89 + graylog_index_id: str = Field(max_length=1024, nullable=False)
90 + graylog_stream_id: str = Field(max_length=1024, nullable=False)
91 + grafana_org_id: str = Field(max_length=1024, nullable=False)
92 + grafana_dashboard_folder_id: str = Field(max_length=1024, nullable=False)
backend/app/integrations/routes.py
+152 -2
@@ -22,6 +22,9 @@ from app.integrations.alert_creation_settings.models.alert_creation_settings imp
22 )
23 from app.integrations.models.customer_integration_settings import AvailableIntegrations
24 from app.integrations.models.customer_integration_settings import CustomerIntegrations
25 +from app.integrations.models.customer_integration_settings import (
26 + CustomerIntegrationsMeta,
27 +)
28 from app.integrations.models.customer_integration_settings import IntegrationAuthKeys
29 from app.integrations.models.customer_integration_settings import IntegrationConfig
30 from app.integrations.models.customer_integration_settings import IntegrationService
@@ -35,6 +38,8 @@ from app.integrations.schema import CreateIntegrationService
38 from app.integrations.schema import CustomerIntegrationCreate
39 from app.integrations.schema import CustomerIntegrationCreateResponse
40 from app.integrations.schema import CustomerIntegrationDeleteResponse
41 +from app.integrations.schema import CustomerIntegrationsMetaResponse
42 +from app.integrations.schema import CustomerIntegrationsMetaSchema
43 from app.integrations.schema import CustomerIntegrationsResponse
44 from app.integrations.schema import DeleteCustomerIntegration
45 from app.integrations.schema import IntegrationWithAuthKeys
@@ -148,6 +153,22 @@ async def check_existing_customer_integration(customer_code: str, integration_na
153 raise HTTPException(status_code=400, detail=f"Customer integration {customer_code} {integration_name} already exists.")
154
155
156 +async def check_existing_customer_integration_meta(customer_code: str, integration_name: str, session: AsyncSession):
157 + """
158 + Check if the customer integration meta already exists for the customer code and integration name.
159 + """
160 + stmt = select(CustomerIntegrationsMeta).where(
161 + CustomerIntegrationsMeta.customer_code == customer_code,
162 + CustomerIntegrationsMeta.integration_name == integration_name,
163 + )
164 + result = await session.execute(stmt)
165 + if result.scalars().first() is not None:
166 + raise HTTPException(
167 + status_code=400,
168 + detail=f"Customer integration meta {customer_code} {integration_name} already exists.",
169 + )
170 +
171 +
172 async def create_integration_service(
173 integration_name: str,
174 settings: CreateIntegrationService,
@@ -216,7 +237,7 @@ async def get_customer_and_service_ids(session, customer_code, integration_name)
237 .join(IntegrationService, IntegrationSubscription.integration_service_id == IntegrationService.id)
238 .where(CustomerIntegrations.customer_code == customer_code, IntegrationService.service_name == integration_name),
239 )
219 - return result.one()
240 + return result.all()
241 except NoResultFound:
242 raise HTTPException(status_code=404, detail="Customer integration not found")
243
@@ -421,6 +442,32 @@ async def get_customer_integrations(session: AsyncSession = Depends(get_db)):
442 )
443
444
445 +@integration_settings_router.get(
446 + "/customer_integrations_meta",
447 + response_model=CustomerIntegrationsMetaResponse,
448 + description="Get a list of customer integrations metadata.",
449 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
450 +)
451 +async def get_customer_integrations_meta(session: AsyncSession = Depends(get_db)):
452 + """
453 + Endpoint to get a list of customer integrations metadata.
454 + """
455 + try:
456 + stmt = select(CustomerIntegrationsMeta)
457 + result = await session.execute(stmt)
458 + customer_integrations_meta = result.scalars().all()
459 + except Exception as e:
460 + logger.error(f"Error while fetching customer integrations metadata: {e}")
461 + customer_integrations_meta = []
462 +
463 + logger.info(f"customer_integrations_meta: {customer_integrations_meta}")
464 + return CustomerIntegrationsMetaResponse(
465 + customer_integrations_meta=customer_integrations_meta,
466 + message="Customer integrations metadata successfully retrieved.",
467 + success=True,
468 + )
469 +
470 +
471 @integration_settings_router.get(
472 "/customer_integrations/{customer_code}",
473 response_model=CustomerIntegrationsResponse,
@@ -454,6 +501,30 @@ async def get_customer_integrations_by_customer_code(
501 )
502
503
504 +@integration_settings_router.get(
505 + "/customer_integrations_meta/{customer_code}",
506 + response_model=CustomerIntegrationsMetaResponse,
507 + description="Get a list of customer integrations metadata for a specific customer.",
508 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
509 +)
510 +async def get_customer_integrations_meta_by_customer_code(
511 + customer_code: str,
512 + session: AsyncSession = Depends(get_db),
513 +):
514 + """
515 + Endpoint to get a list of customer integrations metadata for a specific customer.
516 + """
517 + stmt = select(CustomerIntegrationsMeta).where(CustomerIntegrationsMeta.customer_code == customer_code)
518 + result = await session.execute(stmt)
519 + customer_integrations_meta = result.scalars().all()
520 + logger.info(f"customer_integrations_meta: {customer_integrations_meta}")
521 + return CustomerIntegrationsMetaResponse(
522 + customer_integrations_meta=customer_integrations_meta,
523 + message="Customer integrations metadata successfully retrieved.",
524 + success=True,
525 + )
526 +
527 +
528 @integration_settings_router.post(
529 "/create_integration",
530 response_model=CustomerIntegrationCreateResponse,
@@ -513,6 +584,45 @@ async def create_integration(
584 )
585
586
587 +@integration_settings_router.post(
588 + "/create_integration_meta",
589 + response_model=CustomerIntegrationsMetaResponse,
590 + description="Create a new customer integration metadata.",
591 + # dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
592 +)
593 +async def create_integration_meta(
594 + customer_integration_meta: CustomerIntegrationsMetaSchema,
595 + session: AsyncSession = Depends(get_db),
596 +):
597 + """
598 + Endpoint to create a new customer integration metadata.
599 + """
600 + await validate_customer_code(customer_integration_meta.customer_code, session)
601 + await validate_customer_meta(customer_integration_meta.customer_code, session)
602 + await check_existing_customer_integration_meta(
603 + customer_integration_meta.customer_code,
604 + customer_integration_meta.integration_name,
605 + session,
606 + )
607 + try:
608 + new_customer_integration_meta = CustomerIntegrationsMeta(
609 + **customer_integration_meta.dict(),
610 + )
611 + session.add(new_customer_integration_meta)
612 + await session.commit()
613 + return CustomerIntegrationsMetaResponse(
614 + message="Customer integration metadata successfully created.",
615 + success=True,
616 + )
617 + except Exception as e:
618 + logger.error(f"Error while creating customer integration metadata: {e}")
619 + return CustomerIntegrationsMetaResponse(
620 + customer_integrations_meta=None,
621 + message="Error while creating customer integration metadata.",
622 + success=False,
623 + )
624 +
625 +
626 @integration_settings_router.put(
627 "/update_integration/{customer_code}",
628 response_model=CustomerIntegrationCreateResponse,
@@ -617,7 +727,14 @@ async def delete_integration(
727 customer_code = delete_customer_integration.customer_code
728 integration_name = delete_customer_integration.integration_name
729
620 - customer_id, integration_service_id = await get_customer_and_service_ids(session, customer_code, integration_name)
730 + results = await get_customer_and_service_ids(session, customer_code, integration_name)
731 + # Check if results is not empty
732 + if results:
733 + # Unpack the first tuple in results
734 + customer_id, integration_service_id = results[0]
735 + else:
736 + # Handle the case where results is empty
737 + raise HTTPException(status_code=404, detail="Customer integration not found")
738
739 subscription_ids = await get_subscription_ids(session, customer_id, integration_service_id)
740 if not subscription_ids:
@@ -635,3 +752,36 @@ async def delete_integration(
752 message=f"Customer integration {customer_code} {integration_name} successfully deleted.",
753 success=True,
754 )
755 +
756 +
757 +@integration_settings_router.delete(
758 + "/delete_integration_meta",
759 + response_model=CustomerIntegrationsMetaResponse,
760 + description="Delete a customer integration metadata.",
761 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
762 +)
763 +async def delete_integration_meta(
764 + customer_integration_meta: CustomerIntegrationsMetaSchema,
765 + session: AsyncSession = Depends(get_db),
766 +):
767 + """
768 + Endpoint to delete a customer integration metadata.
769 + """
770 + try:
771 + stmt = delete(CustomerIntegrationsMeta).where(
772 + CustomerIntegrationsMeta.customer_code == customer_integration_meta.customer_code,
773 + CustomerIntegrationsMeta.integration_name == customer_integration_meta.integration_name,
774 + )
775 + await session.execute(stmt)
776 + await session.commit()
777 + return CustomerIntegrationsMetaResponse(
778 + message="Customer integration metadata successfully deleted.",
779 + success=True,
780 + )
781 + except Exception as e:
782 + logger.error(f"Error while deleting customer integration metadata: {e}")
783 + return CustomerIntegrationsMetaResponse(
784 + customer_integrations_meta=None,
785 + message="Error while deleting customer integration metadata.",
786 + success=False,
787 + )
backend/app/integrations/schema.py
+23
@@ -207,3 +207,26 @@ class UpdateCustomerIntegration(BaseModel):
207 ...,
208 description="The integration auth keys.",
209 )
210 +
211 +
212 +class CustomerIntegrationsMetaSchema(BaseModel):
213 + id: Optional[int] = None
214 + customer_code: str
215 + integration_name: str
216 + graylog_input_id: Optional[str] = None
217 + graylog_index_id: str
218 + graylog_stream_id: str
219 + grafana_org_id: str
220 + grafana_dashboard_folder_id: str
221 +
222 + class Config:
223 + orm_mode = True
224 +
225 +
226 +class CustomerIntegrationsMetaResponse(BaseModel):
227 + message: str
228 + success: bool
229 + customer_integrations_meta: Optional[List[CustomerIntegrationsMetaSchema]] = Field(
230 + None,
231 + description="The customer integrations metadata.",
232 + )
backend/app/integrations/utils/utils.py new
+62
@@ -0,0 +1,62 @@
1 +from typing import Dict
2 +
3 +from fastapi import HTTPException
4 +from loguru import logger
5 +from sqlalchemy.ext.asyncio import AsyncSession
6 +
7 +from app.integrations.routes import get_customer_integrations_by_customer_code
8 +from app.integrations.schema import CustomerIntegrations
9 +from app.integrations.schema import CustomerIntegrationsResponse
10 +
11 +
12 +async def get_customer_integration_response(customer_code: str, session: AsyncSession) -> CustomerIntegrationsResponse:
13 + """
14 + Retrieves the integration response for a customer.
15 +
16 + Args:
17 + customer_code (str): The code of the customer.
18 + session (AsyncSession): The async session object for database operations.
19 +
20 + Returns:
21 + CustomerIntegrationsResponse: The integration response for the customer.
22 +
23 + Raises:
24 + HTTPException: If the customer integration settings are not found.
25 + """
26 + customer_integration_response = await get_customer_integrations_by_customer_code(customer_code, session)
27 + if customer_integration_response.available_integrations == []:
28 + raise HTTPException(status_code=404, detail="Customer integration settings not found.")
29 + return customer_integration_response
30 +
31 +
32 +def extract_mimecast_auth_keys(customer_integration: CustomerIntegrations) -> Dict[str, str]:
33 + """
34 + Extracts the authentication keys for Office365 integration from the given customer integration.
35 +
36 + Args:
37 + customer_integration (CustomerIntegrations): The customer integration object.
38 +
39 + Returns:
40 + Dict[str, str]: A dictionary containing the authentication keys for Office365 integration.
41 +
42 + Raises:
43 + HTTPException: If no authentication keys are found for Office365 integration.
44 + """
45 + mimecast_auth_keys = {}
46 + try:
47 + for subscription in customer_integration.integration_subscriptions:
48 + if subscription.integration_service.service_name == "Mimecast":
49 + for auth_key in subscription.integration_auth_keys:
50 + mimecast_auth_keys[auth_key.auth_key_name] = auth_key.auth_value
51 + if not mimecast_auth_keys:
52 + raise HTTPException(
53 + status_code=404,
54 + detail="No auth keys found for Mimecast integration. Please create auth keys for Mimecast integration.",
55 + )
56 + except Exception as e:
57 + logger.error(f"Error extracting auth keys for Mimecast integration: {e}")
58 + raise HTTPException(
59 + status_code=404,
60 + detail="No auth keys found for Mimecast integration. Please create auth keys for Mimecast integration.",
61 + )
62 + return mimecast_auth_keys
backend/app/schedulers/services/agent_sync.py
-24
@@ -5,8 +5,6 @@ import requests
5 from dotenv import load_dotenv
6
7 from app.db.db_session import get_sync_db_session
8 -from app.integrations.mimecast.routes.mimecast import invoke_mimecast_route
9 -from app.integrations.mimecast.schema.mimecast import MimecastRequest
8 from app.schedulers.models.scheduler import JobMetadata
9 from app.schedulers.utils.universal import scheduler_login
10
@@ -47,25 +45,3 @@ def agent_sync():
45 else:
46 # Handle the case where job_metadata does not exist
47 print("JobMetadata for 'agent_sync' not found.")
50 -
51 -
52 -async def invoke_mimecast_integration():
53 - """
54 - Invokes the Mimecast integration.
55 - """
56 - # Use get_sync_db_session to create and manage a synchronous session
57 - with get_sync_db_session() as session:
58 - # Synchronous ORM operations
59 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_mimecast_integration").one_or_none()
60 - if job_metadata:
61 - job_metadata.last_success = datetime.utcnow()
62 - session.add(job_metadata)
63 - session.commit()
64 - else:
65 - # Handle the case where job_metadata does not exist
66 - print("JobMetadata for 'invoke_mimecast_integration' not found.")
67 - # Invoke the Mimecast integration
68 - await invoke_mimecast_route(
69 - MimecastRequest(customer_code="", integration_name="Mimecast"),
70 - session,
71 - )
backend/app/schedulers/services/invoke_mimecast.py
+2
@@ -20,6 +20,7 @@ async def invoke_mimecast_integration() -> MimecastResponse:
20 """
21 Invokes the Mimecast integration.
22 """
23 + logger.info("Invoking Mimecast integration scheduled job.")
24 customer_codes = []
25 async with get_db_session() as session:
26 stmt = select(CustomerIntegrations).where(CustomerIntegrations.integration_service_name == "Mimecast")
@@ -43,6 +44,7 @@ async def invoke_mimecast_integration() -> MimecastResponse:
44 else:
45 # Handle the case where job_metadata does not exist
46 print("JobMetadata for 'invoke_mimecast_integration' not found.")
47 +
48 return MimecastResponse(success=True, message="Mimecast integration invoked.")
49
50
backend/app/schedulers/utils/universal.py
+6 -1
@@ -1,7 +1,12 @@
1 +import os
2 +
3 import requests
4 +from dotenv import load_dotenv
5
6 from app.auth.services.universal import get_scheduler_password
7
8 +load_dotenv()
9 +
10
11 def scheduler_login():
12 """
@@ -16,7 +21,7 @@ def scheduler_login():
21
22 # Get an auth token
23 token_response = requests.post(
19 - "http://localhost:5000/auth/token",
24 + f"http://{os.getenv('SERVER_IP')}:5000/auth/token",
25 headers={"accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"},
26 data={"grant_type": "", "username": "scheduler", "password": password, "scope": ""},
27 )
package-lock.json
+42 -42
@@ -37,7 +37,7 @@
37 "vue": "^3.4.15",
38 "vue-advanced-cropper": "^2.8.8",
39 "vue-highlight-words": "^3.0.1",
40 - "vue-i18n": "^9.9.0",
40 + "vue-i18n": "^9.9.1",
41 "vue-router": "^4.2.5",
42 "vue-sjv": "^0.0.6",
43 "vue3-apexcharts": "^1.4.4",
@@ -56,7 +56,7 @@
56 "@types/lodash": "^4.14.202",
57 "@types/markdown-it": "^13.0.7",
58 "@types/markdown-it-highlightjs": "^3.3.4",
59 - "@types/node": "^20.11.7",
59 + "@types/node": "^20.11.16",
60 "@types/validator": "^13.11.8",
61 "@vitejs/plugin-vue": "^5.0.3",
62 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -65,10 +65,10 @@
65 "@vue/test-utils": "^2.4.4",
66 "@vue/tsconfig": "^0.5.1",
67 "autoprefixer": "^10.4.17",
68 - "cypress": "^13.6.3",
68 + "cypress": "^13.6.4",
69 "eslint": "^8.56.0",
70 "eslint-plugin-cypress": "^2.15.1",
71 - "eslint-plugin-vue": "^9.20.1",
71 + "eslint-plugin-vue": "^9.21.1",
72 "fs-extra": "^11.2.0",
73 "jsdom": "^24.0.0",
74 "json5": "^2.2.3",
@@ -80,13 +80,13 @@
80 "start-server-and-test": "^2.0.3",
81 "tailwind-config-viewer": "^1.7.3",
82 "tailwindcss": "^3.4.1",
83 - "taze": "^0.13.1",
83 + "taze": "^0.13.2",
84 "ts-node": "^10.9.2",
85 "typescript": "~5.3.3",
86 "unplugin-vue-components": "^0.26.0",
87 "vite": "^5.0.12",
88 "vite-bundle-analyzer": "^0.7.0",
89 - "vite-bundle-visualizer": "^1.0.0",
89 + "vite-bundle-visualizer": "^1.0.1",
90 "vite-svg-loader": "^5.1.0",
91 "vitest": "^1.2.2",
92 "vue-tsc": "^1.8.27"
@@ -665,7 +665,7 @@
665 },
666 "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
667 "version": "1.3.0",
668 - "dev": true,
668 + "extraneous": true,
669 "inBundle": true,
670 "license": "MIT",
671 "engines": {
@@ -1360,12 +1360,12 @@
1360 }
1361 },
1362 "node_modules/@intlify/core-base": {
1363 - "version": "9.9.0",
1364 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.9.0.tgz",
1365 - "integrity": "sha512-C7UXPymDIOlMGSNjAhNLtKgzITc/8BjINK5gNKXg8GiWCTwL6n3MWr55czksxn8RM5wTMz0qcLOFT+adtaVQaA==",
1363 + "version": "9.9.1",
1364 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.9.1.tgz",
1365 + "integrity": "sha512-qsV15dg7jNX2faBRyKMgZS8UcFJViWEUPLdzZ9UR0kQZpFVeIpc0AG7ZOfeP7pX2T9SQ5jSiorq/tii9nkkafA==",
1366 "dependencies": {
1367 - "@intlify/message-compiler": "9.9.0",
1368 - "@intlify/shared": "9.9.0"
1367 + "@intlify/message-compiler": "9.9.1",
1368 + "@intlify/shared": "9.9.1"
1369 },
1370 "engines": {
1371 "node": ">= 16"
@@ -1375,11 +1375,11 @@
1375 }
1376 },
1377 "node_modules/@intlify/message-compiler": {
1378 - "version": "9.9.0",
1379 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.9.0.tgz",
1380 - "integrity": "sha512-yDU/jdUm9KuhEzYfS+wuyja209yXgdl1XFhMlKtXEgSFTxz4COZQCRXXbbH8JrAjMsaJ7bdoPSLsKlY6mXG2iA==",
1378 + "version": "9.9.1",
1379 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.9.1.tgz",
1380 + "integrity": "sha512-zTvP6X6HeumHOXuAE1CMMsV6tTX+opKMOxO1OHTCg5N5Sm/F7d8o2jdT6W6L5oHUsJ/vvkGefHIs7Q3hfowmsA==",
1381 "dependencies": {
1382 - "@intlify/shared": "9.9.0",
1382 + "@intlify/shared": "9.9.1",
1383 "source-map-js": "^1.0.2"
1384 },
1385 "engines": {
@@ -1390,9 +1390,9 @@
1390 }
1391 },
1392 "node_modules/@intlify/shared": {
1393 - "version": "9.9.0",
1394 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.9.0.tgz",
1395 - "integrity": "sha512-1ECUyAHRrzOJbOizyGufYP2yukqGrWXtkmTu4PcswVnWbkcjzk3YQGmJ0bLkM7JZ0ZYAaohLGdYvBYnTOGYJ9g==",
1393 + "version": "9.9.1",
1394 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.9.1.tgz",
1395 + "integrity": "sha512-b3Pta1nwkz5rGq434v0psHwEwHGy1pYCttfcM22IE//K9owbpkEvFptx9VcuRAxjQdrO2If249cmDDjBu5wMDA==",
1396 "engines": {
1397 "node": ">= 16"
1398 },
@@ -2407,9 +2407,9 @@
2407 "dev": true
2408 },
2409 "node_modules/@types/node": {
2410 - "version": "20.11.7",
2411 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz",
2412 - "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==",
2410 + "version": "20.11.16",
2411 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz",
2412 + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==",
2413 "dev": true,
2414 "dependencies": {
2415 "undici-types": "~5.26.4"
@@ -4496,9 +4496,9 @@
4496 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4497 },
4498 "node_modules/cypress": {
4499 - "version": "13.6.3",
4500 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.3.tgz",
4501 - "integrity": "sha512-d/pZvgwjAyZsoyJ3FOsJT5lDsqnxQ/clMqnNc++rkHjbkkiF2h9s0JsZSyyH4QXhVFW3zPFg82jD25roFLOdZA==",
4499 + "version": "13.6.4",
4500 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz",
4501 + "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==",
4502 "dev": true,
4503 "hasInstallScript": true,
4504 "dependencies": {
@@ -5571,16 +5571,16 @@
5571 }
5572 },
5573 "node_modules/eslint-plugin-vue": {
5574 - "version": "9.20.1",
5575 - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.20.1.tgz",
5576 - "integrity": "sha512-GyCs8K3lkEvoyC1VV97GJhP1SvqsKCiWGHnbn0gVUYiUhaH2+nB+Dv1uekv1THFMPbBfYxukrzQdltw950k+LQ==",
5574 + "version": "9.21.1",
5575 + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz",
5576 + "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==",
5577 "dependencies": {
5578 "@eslint-community/eslint-utils": "^4.4.0",
5579 "natural-compare": "^1.4.0",
5580 "nth-check": "^2.1.1",
5581 "postcss-selector-parser": "^6.0.13",
5582 "semver": "^7.5.4",
5583 - "vue-eslint-parser": "^9.4.0",
5583 + "vue-eslint-parser": "^9.4.2",
5584 "xml-name-validator": "^4.0.0"
5585 },
5586 "engines": {
@@ -11530,18 +11530,18 @@
11530 "dev": true
11531 },
11532 "node_modules/taze": {
11533 - "version": "0.13.1",
11534 - "resolved": "https://registry.npmjs.org/taze/-/taze-0.13.1.tgz",
11535 - "integrity": "sha512-viIeTWEezE1mtQ9M9dtvIwfF7WkNRXVZP7g/NsGDFxwwzJIVJReSiZiyfsJccdJo6XUe7FaYzvrG1VD6Tnj4Jg==",
11533 + "version": "0.13.2",
11534 + "resolved": "https://registry.npmjs.org/taze/-/taze-0.13.2.tgz",
11535 + "integrity": "sha512-G2S3f4kFjcYu0Xdxv/vgx5NmPuJAhhI16weQs0P8J/pjCo4Ieip37k1/TPg32pgggLq1GQtwlDuAui/LQ+XH9g==",
11536 "dev": true,
11537 "dependencies": {
11538 "@antfu/ni": "^0.21.12",
11539 - "@npmcli/config": "^8.0.3",
11539 + "@npmcli/config": "^8.1.0",
11540 "cli-progress": "^3.12.0",
11541 "deepmerge": "^4.3.1",
11542 "detect-indent": "^7.0.1",
11543 "execa": "^8.0.1",
11544 - "pacote": "^17.0.5",
11544 + "pacote": "^17.0.6",
11545 "picocolors": "^1.0.0",
11546 "prompts": "^2.4.2",
11547 "semver": "^7.5.4",
@@ -12457,9 +12457,9 @@
12457 }
12458 },
12459 "node_modules/vite-bundle-visualizer": {
12460 - "version": "1.0.0",
12461 - "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.0.0.tgz",
12462 - "integrity": "sha512-25+1XydP08lE373O0kHn/9C/n0A8NM4CEpWAXhObemTXYnSOnil++bXUTIeNCMA26fU9CBebUI7+Nj/OfL+2JA==",
12460 + "version": "1.0.1",
12461 + "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.0.1.tgz",
12462 + "integrity": "sha512-JdUu5viGyw7K1HMstqaAN7y1rnNz93srGeF7FJgFCzM7NL1nH/QlpywDA296qv/KjPPPsq60mOJhtXddikVKSA==",
12463 "dev": true,
12464 "dependencies": {
12465 "cac": "^6.7.14",
@@ -12810,12 +12810,12 @@
12810 }
12811 },
12812 "node_modules/vue-i18n": {
12813 - "version": "9.9.0",
12814 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.9.0.tgz",
12815 - "integrity": "sha512-xQ5SxszUAqK5n84N+uUyHH/PiQl9xZ24FOxyAaNonmOQgXeN+rD9z/6DStOpOxNFQn4Cgcquot05gZc+CdOujA==",
12813 + "version": "9.9.1",
12814 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.9.1.tgz",
12815 + "integrity": "sha512-xyQ4VspLdNSPTKBFBPWa1tvtj+9HuockZwgFeD2OhxxXuC2CWeNvV4seu2o9+vbQOyQbhAM5Ez56oxUrrnTWdw==",
12816 "dependencies": {
12817 - "@intlify/core-base": "9.9.0",
12818 - "@intlify/shared": "9.9.0",
12817 + "@intlify/core-base": "9.9.1",
12818 + "@intlify/shared": "9.9.1",
12819 "@vue/devtools-api": "^6.5.0"
12820 },
12821 "engines": {
package.json
+6 -6
@@ -61,7 +61,7 @@
61 "vue": "^3.4.15",
62 "vue-advanced-cropper": "^2.8.8",
63 "vue-highlight-words": "^3.0.1",
64 - "vue-i18n": "^9.9.0",
64 + "vue-i18n": "^9.9.1",
65 "vue-router": "^4.2.5",
66 "vue-sjv": "^0.0.6",
67 "vue3-apexcharts": "^1.4.4",
@@ -80,7 +80,7 @@
80 "@types/lodash": "^4.14.202",
81 "@types/markdown-it": "^13.0.7",
82 "@types/markdown-it-highlightjs": "^3.3.4",
83 - "@types/node": "^20.11.7",
83 + "@types/node": "^20.11.16",
84 "@types/validator": "^13.11.8",
85 "@vitejs/plugin-vue": "^5.0.3",
86 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -89,10 +89,10 @@
89 "@vue/test-utils": "^2.4.4",
90 "@vue/tsconfig": "^0.5.1",
91 "autoprefixer": "^10.4.17",
92 - "cypress": "^13.6.3",
92 + "cypress": "^13.6.4",
93 "eslint": "^8.56.0",
94 "eslint-plugin-cypress": "^2.15.1",
95 - "eslint-plugin-vue": "^9.20.1",
95 + "eslint-plugin-vue": "^9.21.1",
96 "fs-extra": "^11.2.0",
97 "jsdom": "^24.0.0",
98 "json5": "^2.2.3",
@@ -104,13 +104,13 @@
104 "start-server-and-test": "^2.0.3",
105 "tailwind-config-viewer": "^1.7.3",
106 "tailwindcss": "^3.4.1",
107 - "taze": "^0.13.1",
107 + "taze": "^0.13.2",
108 "ts-node": "^10.9.2",
109 "typescript": "~5.3.3",
110 "unplugin-vue-components": "^0.26.0",
111 "vite": "^5.0.12",
112 "vite-bundle-analyzer": "^0.7.0",
113 - "vite-bundle-visualizer": "^1.0.0",
113 + "vite-bundle-visualizer": "^1.0.1",
114 "vite-svg-loader": "^5.1.0",
115 "vitest": "^1.2.2",
116 "vue-tsc": "^1.8.27"
src/api/integrations.ts
+6
@@ -55,6 +55,12 @@ export default {
55 })
56 },
57
58 + mimecastProvision(customerCode: string, integrationName: string) {
59 + return HttpClient.post<FlaskBaseResponse>(`/mimecast/provision`, {
60 + customer_code: customerCode,
61 + integration_name: integrationName
62 + })
63 + },
64 office365Provision(customerCode: string, integrationName: string) {
65 return HttpClient.post<FlaskBaseResponse>(`/office365/provision`, {
66 customer_code: customerCode,
src/components/customers/integrations/CustomerIntegrationActions.vue
+36 -1
@@ -12,6 +12,18 @@
12 Deploy
13 </n-button>
14
15 + <n-button
16 + v-if="isMimecast && !integration.deployed"
17 + :loading="loadingMimecastProvision"
18 + @click="mimecastProvision()"
19 + type="success"
20 + :size="size"
21 + secondary
22 + >
23 + <template #icon><Icon :name="DeployIcon"></Icon></template>
24 + Deploy
25 + </n-button>
26 +
27 <n-button
28 :size="size"
29 type="error"
@@ -55,10 +67,12 @@ const DeleteIcon = "ph:trash"
67 const dialog = useDialog()
68 const message = useMessage()
69 const loadingOffice365Provision = ref(false)
70 +const loadingMimecastProvision = ref(false)
71 const loadingDelete = ref(false)
59 -const loading = computed(() => loadingOffice365Provision.value || loadingDelete.value)
72 +const loading = computed(() => loadingMimecastProvision.value || loadingOffice365Provision.value || loadingDelete.value)
73
74 const isOffice365 = computed(() => serviceName.value === "Office365")
75 +const isMimecast = computed(() => serviceName.value === "Mimecast")
76 const serviceName = computed(() => integration.integration_service_name)
77 const customerCode = computed(() => integration.customer_code)
78
@@ -87,6 +101,27 @@ function office365Provision() {
101 })
102 }
103
104 +function mimecastProvision() {
105 + loadingMimecastProvision.value = true
106 +
107 + Api.integrations
108 + .mimecastProvision(customerCode.value, serviceName.value)
109 + .then(res => {
110 + if (res.data.success) {
111 + emit("deployed")
112 + message.success(res.data?.message || "Customer integration successfully deployed.")
113 + } else {
114 + message.warning(res.data?.message || "An error occurred. Please try again later.")
115 + }
116 + })
117 + .catch(err => {
118 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
119 + })
120 + .finally(() => {
121 + loadingMimecastProvision.value = false
122 + })
123 +}
124 +
125 function handleDelete() {
126 dialog.warning({
127 title: "Confirm",
vite.config.mts
+10
@@ -4,6 +4,7 @@ import vue from "@vitejs/plugin-vue"
4 import vueJsx from "@vitejs/plugin-vue-jsx"
5 import svgLoader from "vite-svg-loader"
6 import Components from "unplugin-vue-components/vite"
7 +const hash = Math.floor(Math.random() * 90000) + 10000
8 // import { analyzer } from "vite-bundle-analyzer"
9
10 // https://vitejs.dev/config/
@@ -29,6 +30,15 @@ export default defineConfig({
30 "@": fileURLToPath(new URL("./src", import.meta.url))
31 }
32 },
33 + build: {
34 + rollupOptions: {
35 + output: {
36 + entryFileNames: `[name]` + hash + `.js`,
37 + chunkFileNames: `[name]` + hash + `.js`,
38 + assetFileNames: `[name]` + hash + `.[ext]`
39 + }
40 + }
41 + },
42 optimizeDeps: {
43 include: ["fast-deep-equal"]
44 }