aixtools 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aixtools might be problematic. Click here for more details.

aixtools/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.8'
32
- __version_tuple__ = version_tuple = (0, 1, 8)
31
+ __version__ = version = '0.1.9'
32
+ __version_tuple__ = version_tuple = (0, 1, 9)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,136 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from fastmcp import Context
5
+
6
+ from aixtools.server.path import get_workspace_path
7
+
8
+ PRIVATE_DATA_FILE = ".private_data"
9
+
10
+
11
+ class PrivateData:
12
+ """
13
+ Class to manage private data file in the workspace.
14
+
15
+ The information is stored in a JSON file named `.private_data` within the workspace directory.
16
+ If the file does not exist, it indicates that there is no private data.
17
+
18
+ IMPORTANT: All modifications save the data to the file immediately.
19
+
20
+ FIXME: We should add some level of mutex/locking to prevent concurrent writes.
21
+ """
22
+
23
+ def __init__(self, ctx: Context | None = None):
24
+ self.ctx: Context | None = ctx
25
+ self._has_private_data: bool = False # Flag indicating if private data exists
26
+ self._private_datasets: list[str] = [] # List of private datasets
27
+ self._idap_datasets: list[str] = [] # List of dataset with IDAP
28
+ self.load()
29
+
30
+ def add_private_dataset(self, dataset_name: str) -> None:
31
+ """
32
+ Add a private dataset to the list.
33
+ Save the state after modification.
34
+ """
35
+ if dataset_name not in self._private_datasets:
36
+ self._private_datasets.append(dataset_name)
37
+ self._has_private_data = True
38
+ self.save()
39
+
40
+ def add_idap_dataset(self, dataset_name: str) -> None:
41
+ """
42
+ Add a dataset with IDAP to the list.
43
+ This also adds it to the private datasets if not already present.
44
+ Save the state after modification.
45
+ """
46
+ if not self.has_idap_dataset(dataset_name):
47
+ self._idap_datasets.append(dataset_name)
48
+ self._has_private_data = True
49
+ # An IDAP dataset is also a private dataset
50
+ if not self.has_private_dataset(dataset_name):
51
+ self._private_datasets.append(dataset_name)
52
+ self.save()
53
+
54
+ def get_private_datasets(self) -> list[str]:
55
+ """Get the list of private datasets as a copy (to avoid modification)."""
56
+ return list(self._private_datasets)
57
+
58
+ def get_idap_datasets(self) -> list[str]:
59
+ """Get the list of datasets with IDAP as a copy (to avoid modification)."""
60
+ return list(self._idap_datasets)
61
+
62
+ def has_private_dataset(self, dataset_name: str) -> bool:
63
+ """Check if a specific private dataset exists."""
64
+ return dataset_name in self._private_datasets
65
+
66
+ def has_idap_dataset(self, dataset_name: str) -> bool:
67
+ """Check if a specific dataset with IDAP exists."""
68
+ return dataset_name in self._idap_datasets
69
+
70
+ @property
71
+ def has_private_data(self) -> bool:
72
+ """Check if private data exists."""
73
+ return self._has_private_data
74
+
75
+ @has_private_data.setter
76
+ def has_private_data(self, value: bool) -> None:
77
+ """
78
+ Set the flag indicating if private data exists.
79
+ Save the state after modification.
80
+ """
81
+ self._has_private_data = value
82
+ if not value:
83
+ self._private_datasets = []
84
+ self._idap_datasets = []
85
+ self.save()
86
+
87
+ def _get_private_data_path(self) -> Path:
88
+ """Get the path to the private data file in the workspace."""
89
+ return get_workspace_path(service_name=None, ctx=self.ctx) / PRIVATE_DATA_FILE
90
+
91
+ def _has_private_data_file(self) -> bool:
92
+ """Check if the private data file exists in the workspace."""
93
+ private_data_path = self.get_private_data_path() # type: ignore
94
+ return private_data_path.exists()
95
+
96
+ def save(self) -> None:
97
+ """Save content to the private data file in the workspace."""
98
+ private_data_path = self._get_private_data_path()
99
+ # No private data? Delete the file if it exists
100
+ if not self.has_private_data:
101
+ private_data_path.unlink(missing_ok=True)
102
+ return
103
+ # If there is private data, serialize this object as JSON
104
+ private_data_path.parent.mkdir(parents=True, exist_ok=True)
105
+ with open(private_data_path, "w") as f:
106
+ # Dump class as JSON, excluding the context
107
+ data_dict = self.__dict__.copy()
108
+ data_dict["ctx"] = None
109
+ json_data = json.dumps(data_dict, indent=4)
110
+ f.write(json_data)
111
+
112
+ def load(self) -> None:
113
+ """Load content from the private data file in the workspace."""
114
+ private_data_path = self._get_private_data_path()
115
+ if not private_data_path.exists():
116
+ # No private data file
117
+ self.has_private_data = False
118
+ self._private_datasets = []
119
+ self._idap_datasets = []
120
+ return
121
+ with open(private_data_path, "r") as f:
122
+ data = json.load(f)
123
+ self.has_private_data = data.get("_has_private_data", False)
124
+ self._private_datasets = data.get("_private_datasets", [])
125
+ self._idap_datasets = data.get("_idap_datasets", [])
126
+
127
+ def __repr__(self) -> str:
128
+ return (
129
+ f"PrivateData(has_private_data={self.has_private_data}, "
130
+ f"private_datasets={self._private_datasets}, "
131
+ f"idap_datasets={self._idap_datasets}), "
132
+ f"file_path={self._get_private_data_path()})"
133
+ )
134
+
135
+ def __str__(self) -> str:
136
+ return self.__repr__()
@@ -13,6 +13,10 @@ from .utils import (
13
13
  get_session_id_tuple,
14
14
  run_in_thread,
15
15
  )
16
+ from .workspace_privacy import (
17
+ is_session_private,
18
+ set_session_private,
19
+ )
16
20
 
17
21
  __all__ = [
18
22
  "get_workspace_path",
@@ -20,4 +24,6 @@ __all__ = [
20
24
  "container_to_host_path",
21
25
  "host_to_container_path",
22
26
  "run_in_thread",
27
+ "is_session_private",
28
+ "set_session_private",
23
29
  ]
@@ -0,0 +1,65 @@
1
+ """
2
+ Workspace privacy utilities for managing session-level privacy flags.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ from fastmcp import Context
8
+
9
+ from aixtools.logging.logging_config import get_logger
10
+ from aixtools.server.path import get_workspace_path
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ PRIVACY_FLAG_FILENAME = ".private_data_indicator"
15
+
16
+
17
+ def set_session_private(ctx: Context | tuple[str, str] | None = None) -> bool:
18
+ """
19
+ Set the current session as private by creating a privacy flag file.
20
+
21
+ Creates an empty file in the session workspace directory
22
+ and sets it as read-only to prevent accidental removal
23
+
24
+ Args:
25
+ ctx: FastMCP context for user/session identification.
26
+ If None, uses current FastMCP request context from HTTP headers.
27
+ If tuple, first part is a user id (username), second part is session id (aka conversation id)
28
+
29
+ Returns:
30
+ bool: True if privacy flag was successfully created, False otherwise.
31
+ """
32
+ try:
33
+ workspace_path = Path(get_workspace_path(ctx=ctx))
34
+ privacy_file = workspace_path / PRIVACY_FLAG_FILENAME
35
+ workspace_path.mkdir(parents=True, exist_ok=True)
36
+ privacy_file.touch(exist_ok=True)
37
+ privacy_file.chmod(0o444)
38
+ logger.warning("Session marked as private")
39
+ return True
40
+ except (OSError, ValueError, RuntimeError) as e:
41
+ logger.error("Set current session as private: %s", str(e))
42
+ return False
43
+
44
+
45
+ def is_session_private(ctx: Context | tuple[str, str] | None = None) -> bool:
46
+ """
47
+ Check if the current session is marked as private.
48
+
49
+ Args:
50
+ ctx: FastMCP context for user/session identification.
51
+ If None, uses current FastMCP request context from HTTP headers.
52
+ If tuple, first part is a user id (username), second part is session id (aka conversation id)
53
+
54
+ Returns:
55
+ bool: True if workspace is private (flag file exists), False otherwise.
56
+ """
57
+ try:
58
+ workspace_path = Path(get_workspace_path(ctx=ctx))
59
+ privacy_file = workspace_path / PRIVACY_FLAG_FILENAME
60
+ is_private = privacy_file.exists()
61
+ logger.info("Session privacy check, is private: %s", str(is_private))
62
+ return is_private
63
+ except (OSError, ValueError, RuntimeError) as e:
64
+ logger.error("Check session privacy: %s, assuming not private!", str(e))
65
+ return False
aixtools/vault/vault.py CHANGED
@@ -27,9 +27,11 @@ class VaultClient:
27
27
  if not self.client.is_authenticated():
28
28
  raise VaultAuthError("Vault client authentication failed. Check vault_token.")
29
29
 
30
- def _get_secret_path(self, user_id: str, service_name: str) -> str:
31
- """Generate the vault secret path for a user and service."""
32
- return f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}/{service_name}"
30
+ def _get_secret_path(self, user_id: str, service_name: Optional[str] = None) -> str:
31
+ """Generate the vault secret path for a user and optionally a service."""
32
+ if service_name:
33
+ return f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}/{service_name}"
34
+ return f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}"
33
35
 
34
36
  def store_user_service_api_key(self, *, user_id: str, service_name: str, user_api_key: str):
35
37
  """
@@ -98,3 +100,38 @@ class VaultClient:
98
100
  except Exception as e:
99
101
  logger.error("Failed to read complete secret from path %s: %s", secret_path, str(e))
100
102
  raise VaultAuthError(e) from e
103
+
104
+ def list_user_secret_keys(self, *, user_id: str) -> list[str]:
105
+ """
106
+ List all secret keys (service names) for a user, optionally filtered by service name.
107
+
108
+ Args:
109
+ user_id: The user ID to list secrets for
110
+ service_name: Optional service name to filter results. If provided, returns only this service if it exists.
111
+
112
+ Returns:
113
+ List of service names (secret keys) for the user. Empty list if no secrets exist.
114
+ """
115
+ try:
116
+ # List all services for user
117
+ user_path = self._get_secret_path(user_id)
118
+ logger.info("Listing secret keys for user at path %s", user_path)
119
+
120
+ response = self.client.secrets.kv.v2.list_secrets(path=user_path, mount_point=config.VAULT_MOUNT_POINT)
121
+
122
+ if response and "data" in response and "keys" in response["data"]:
123
+ secret_keys = response["data"]["keys"]
124
+ # Remove trailing slashes from directory names if any
125
+ secret_keys = [key.rstrip("/") for key in secret_keys]
126
+ logger.info("Found %d secret keys for user %s", len(secret_keys), user_id)
127
+ return secret_keys
128
+ logger.info("No secret keys found for user %s", user_id)
129
+ return []
130
+
131
+ except InvalidPath:
132
+ # User path does not exist
133
+ logger.warning("User path does not exist for user %s", user_id)
134
+ return []
135
+ except Exception as e:
136
+ logger.error("Failed to list secret keys for user %s: %s", user_id, str(e))
137
+ raise VaultAuthError(e) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aixtools
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Tools for AI exploration and debugging
5
5
  Requires-Python: >=3.11.2
6
6
  Description-Content-Type: text/markdown
@@ -1,5 +1,5 @@
1
1
  aixtools/__init__.py,sha256=9NGHm7LjsQmsvjTZvw6QFJexSvAU4bCoN_KBk9SCa00,260
2
- aixtools/_version.py,sha256=Zaz3s9gl_rzsS46-ymJOALojMxviW77EJq_agE8knLk,704
2
+ aixtools/_version.py,sha256=ib8ckvf-NNDfacXd8unW0p5cf-gl57XyQvjoEMc_pvc,704
3
3
  aixtools/app.py,sha256=JzQ0nrv_bjDQokllIlGHOV0HEb-V8N6k_nGQH-TEsVU,5227
4
4
  aixtools/chainlit.md,sha256=yC37Ly57vjKyiIvK4oUvf4DYxZCwH7iocTlx7bLeGLU,761
5
5
  aixtools/context.py,sha256=I_MD40ZnvRm5WPKAKqBUAdXIf8YaurkYUUHSVVy-QvU,598
@@ -29,6 +29,7 @@ aixtools/agents/__init__.py,sha256=MAW196S2_G7uGqv-VNjvlOETRfuV44WlU1leO7SiR0A,2
29
29
  aixtools/agents/agent.py,sha256=E1zu70t53RqIbcLI_R09wUtsiYZR1bTnElCQ5PrsrKw,6127
30
30
  aixtools/agents/agent_batch.py,sha256=0Zu9yNCRPAQZPjXQ-dIUAmP1uGTVbxVt7xvnMpoJMjU,2251
31
31
  aixtools/agents/prompt.py,sha256=VCOVSnhNKsPIT347ouzwM1PH4I9UTm2cSnTh3ZpjRwk,3391
32
+ aixtools/compliance/private_data.py,sha256=aYEd13eHUajs72nQxCXsa7tklXnqyurUaYuNzm8belQ,5258
32
33
  aixtools/db/__init__.py,sha256=b8vRhme3egV-aUZbAntnOaDkSXB8UT0Xy5oqQhU_z0Q,399
33
34
  aixtools/db/database.py,sha256=caWe95GlxZYlxn2ubDmR-_cQUW0ulkpR3BHunKIaOsw,3369
34
35
  aixtools/db/vector_db.py,sha256=be4JGyXj3o8VEfy9L6SO1aAoDET_zazMJkYfjlYHTYQ,4133
@@ -56,10 +57,11 @@ aixtools/mcp/example_server.py,sha256=1SWCyrLWsAnOa81HC4QbPJo_lBVu0b3SZBWI-qDh1v
56
57
  aixtools/mcp/fast_mcp_log.py,sha256=XYOS406dVjn5YTHyGRsRvVNQ0SKlRObfrKj6EeLFjHg,1057
57
58
  aixtools/mcp/faulty_mcp.py,sha256=uU9vlNGCS_i2k20wocVMaDHTlYjMQxuzjILad9O1cjA,12807
58
59
  aixtools/model_patch/model_patch.py,sha256=JT-oHubIn2LeoSwWbzEQ5vLH7crJmFUecHyQfaAFHa0,1813
59
- aixtools/server/__init__.py,sha256=rwPx020YpOzCnrxA80Lc4yLLcIp-Mpe9hNqVO9wDPv0,448
60
+ aixtools/server/__init__.py,sha256=37ADJrGLzsmjFsM2ZKUoM9cevH8rBn359WesDxIwoco,585
60
61
  aixtools/server/app_mounter.py,sha256=0tJ0tC140ezAjnYdlhpLJQjY-TO8NVw7D8LseYCCVY8,3336
61
62
  aixtools/server/path.py,sha256=SaIJxvmhJy3kzx5zJ6d4cKP6kKu2wFFciQkOLGTA4gg,3056
62
63
  aixtools/server/utils.py,sha256=tZWITIx6M-luV9yve4j3rPtYGSSA6zWS0JWEAySne_M,2276
64
+ aixtools/server/workspace_privacy.py,sha256=grcj82eHSd7gFbb5f_w9nv4TWp50QyU952l0iIPoChM,2375
63
65
  aixtools/testing/__init__.py,sha256=mlmaAR2gmS4SbsYNCxnIprmFpFp-syjgVUkpUszo3mE,166
64
66
  aixtools/testing/aix_test_model.py,sha256=dlI3sdyvmu4fUs_K4-oazs_a7cE6V-gnI6RQ0_fPVxg,5925
65
67
  aixtools/testing/mock_tool.py,sha256=4I0LxxSkLhGIKM2YxCP3cnYI8IYJjdKhfwGZ3dioXsM,2465
@@ -77,8 +79,9 @@ aixtools/utils/utils.py,sha256=5911Ej1ES2NU_FKIWA3CWKhKnwgjvi1aDR2aiD6Xv3E,4880
77
79
  aixtools/utils/chainlit/cl_agent_show.py,sha256=vaRuowp4BRvhxEr5hw0zHEJ7iaSF_5bo_9BH7pGPPpw,4398
78
80
  aixtools/utils/chainlit/cl_utils.py,sha256=fxaxdkcZg6uHdM8uztxdPowg3a2f7VR7B26VPY4t-3c,5738
79
81
  aixtools/vault/__init__.py,sha256=fsr_NuX3GZ9WZ7dGfe0gp_5-z3URxAfwVRXw7Xyc0dU,141
80
- aixtools/vault/vault.py,sha256=WkzBTEYM-Vqjyoa5x5imbEDi0ePBklMjD_aAdvIK-34,4293
81
- docker/mcp-base/Dockerfile,sha256=UuAA1ltJpusKwow2fd_wS0M4DexbxyCzQGNVOK1_lzs,1547
82
+ aixtools/vault/vault.py,sha256=9dZLWdZQk9qN_Q9Djkofw9LUKnJqnrX5H0fGusVLBhA,6037
83
+ docker/mcp-base/Dockerfile,sha256=uislVoTEgRF--AAiyX24sBxlDdfA1ZU5rDM94XYFqvI,1388
84
+ docker/mcp-base/zscaler.crt,sha256=fCUNiOfJlWTA7R4zV1Xyb-XC1_nMHPoYTFGvBj1oz6s,1732
82
85
  notebooks/example_faulty_mcp_server.ipynb,sha256=b2Cy3GXfj-gOBZ7SoUzj25F1rxp5u-32EWPHWQ-sxn8,1729
83
86
  notebooks/example_mcp_server_stdio.ipynb,sha256=ya4dRKNFU2vQxob-uIhKHGAzINXGQ6MehgKVmSCpHLk,1634
84
87
  notebooks/example_raw_mcp_client.ipynb,sha256=uchaG-LuuwEpE2oIkmhZ2s1EDb19AgT1fUv2Jxtjgu8,1795
@@ -89,7 +92,7 @@ scripts/log_view.sh,sha256=bp8oXFRRbbHpyvHAN85wfDHTVK7vMJOYsBx_-bgECQc,511
89
92
  scripts/run_example_mcp_server.sh,sha256=f7m7h7O_wo6-nAsYlOXVWIASCOh3Qbuu0XWizlxMhl8,355
90
93
  scripts/run_faulty_mcp_server.sh,sha256=u_-8NbPDnJQt6IinNSjh8tc2ed-_MjGyipJXrUXaGR8,291
91
94
  scripts/run_server.sh,sha256=5iiB9bB5M2MuOgxVQqu7Oa_tBVtJpt0uB4z9uLu2J50,720
92
- scripts/test.sh,sha256=5akwfnX7P-98898WCXcQRFQt84UBgms5Y9fXC_ym1a4,662
95
+ scripts/test.sh,sha256=KxXWkVqctFRNP8hItJr8K27nDHEkfwNWb1UFhpBQDOk,865
93
96
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
97
  tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
98
  tests/unit/a2a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -102,6 +105,7 @@ tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py,sha256=PcyC
102
105
  tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py,sha256=tb67pFfvyWSaDfKaiPDNBQfl6-o17WtCMZh3lQHrYxY,5468
103
106
  tests/unit/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
107
  tests/unit/agents/test_prompt.py,sha256=YWFZdH_F774hxw79gsWoTWBPVs8UjOAtJOgNXJ8N9gs,15384
108
+ tests/unit/compliance/test_private_data.py,sha256=GjH7NCp54Bz1S-CmH_mUe53lb53kllOOJEm448OniRI,13693
105
109
  tests/unit/google/__init__.py,sha256=eRYHldBi5cFWL7oo2_t5TErI8ESmIjNvBZIcp-w8hSA,45
106
110
  tests/unit/google/test_client.py,sha256=fXR4Cozea7bdL2prM-1s9IqUQ9AheklQnHpN-4YM3gg,11005
107
111
  tests/unit/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -112,9 +116,9 @@ tests/unit/server/test_utils.py,sha256=kvhzdgNfsJl5tqcRBWg2yTR5GPpyrFCOmEIOuHb39
112
116
  tests/unit/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
117
  tests/unit/utils/test_files.py,sha256=AKFmXQqXstyKd2PreE4EmQyhQYeqOmu1Sp80MwHrf_Q,5782
114
118
  tests/unit/vault/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
- tests/unit/vault/test_vault.py,sha256=R_RTDsralvE0JhuIrHQTb85gU9ipDJD7aoYIMdcg0o4,7264
116
- aixtools-0.1.8.dist-info/METADATA,sha256=aVW6zyI3cqprsWoTDOJCwXc-2tD2pMHb2bR6fc4Mo2w,18569
117
- aixtools-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
118
- aixtools-0.1.8.dist-info/entry_points.txt,sha256=dHoutULEZx7xXSqJrZdViSVjfInJibfLibi2nRXL3SE,56
119
- aixtools-0.1.8.dist-info/top_level.txt,sha256=ee4eF-0pqu45zCUVml0mWIhnXQgqMQper2-49BBVHLY,40
120
- aixtools-0.1.8.dist-info/RECORD,,
119
+ tests/unit/vault/test_vault.py,sha256=T9V2Opxl3N5sJPftw0Q4lnVOs6urGpAmffe0cz6PUfw,10445
120
+ aixtools-0.1.9.dist-info/METADATA,sha256=NyTMeH1c44cDNbXW7VKy2feT-xConc_uGIx0gSa68KM,18569
121
+ aixtools-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
122
+ aixtools-0.1.9.dist-info/entry_points.txt,sha256=dHoutULEZx7xXSqJrZdViSVjfInJibfLibi2nRXL3SE,56
123
+ aixtools-0.1.9.dist-info/top_level.txt,sha256=ee4eF-0pqu45zCUVml0mWIhnXQgqMQper2-49BBVHLY,40
124
+ aixtools-0.1.9.dist-info/RECORD,,
@@ -1,17 +1,14 @@
1
1
  FROM ubuntu:22.04
2
2
 
3
3
  RUN apt-get -y update && \
4
- apt-get -y install git curl sudo ca-certificates
4
+ apt-get -y install ca-certificates curl gcc git libcap2-bin sudo
5
+ RUN mv /usr/bin/sudo /usr/sbin
5
6
 
6
- # Build argument for custom certificate file (optional)
7
- ARG CUSTOM_CERT_FILE
8
- # Copy custom certificate if specified
9
- RUN if [ -n "$CUSTOM_CERT_FILE" ] && [ -f "$CUSTOM_CERT_FILE" ]; then \
10
- cp "$CUSTOM_CERT_FILE" /usr/local/share/ca-certificates/ && \
11
- update-ca-certificates && \
12
- export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt && \
13
- export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt; \
14
- fi
7
+ # Add Zscaler CA certificate
8
+ COPY ./zscaler.crt /usr/local/share/ca-certificates/
9
+ RUN update-ca-certificates
10
+ ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
11
+ ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
15
12
 
16
13
  # Install `uv` Python package manager
17
14
  RUN bash -o pipefail -c "curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh"
@@ -0,0 +1,28 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIE0zCCA7ugAwIBAgIJANu+mC2Jt3uTMA0GCSqGSIb3DQEBCwUAMIGhMQswCQYD
3
+ VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMIU2FuIEpvc2Ux
4
+ FTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMMWnNjYWxlciBJbmMuMRgw
5
+ FgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRA
6
+ enNjYWxlci5jb20wHhcNMTQxMjE5MDAyNzU1WhcNNDIwNTA2MDAyNzU1WjCBoTEL
7
+ MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCFNhbiBK
8
+ b3NlMRUwEwYDVQQKEwxac2NhbGVyIEluYy4xFTATBgNVBAsTDFpzY2FsZXIgSW5j
9
+ LjEYMBYGA1UEAxMPWnNjYWxlciBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNzdXBw
10
+ b3J0QHpzY2FsZXIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
11
+ qT7STSxZRTgEFFf6doHajSc1vk5jmzmM6BWuOo044EsaTc9eVEV/HjH/1DWzZtcr
12
+ fTj+ni205apMTlKBW3UYR+lyLHQ9FoZiDXYXK8poKSV5+Tm0Vls/5Kb8mkhVVqv7
13
+ LgYEmvEY7HPY+i1nEGZCa46ZXCOohJ0mBEtB9JVlpDIO+nN0hUMAYYdZ1KZWCMNf
14
+ 5J/aTZiShsorN2A38iSOhdd+mcRM4iNL3gsLu99XhKnRqKoHeH83lVdfu1XBeoQz
15
+ z5V6gA3kbRvhDwoIlTBeMa5l4yRdJAfdpkbFzqiwSgNdhbxTHnYYorDzKfr2rEFM
16
+ dsMU0DHdeAZf711+1CunuQIDAQABo4IBCjCCAQYwHQYDVR0OBBYEFLm33UrNww4M
17
+ hp1d3+wcBGnFTpjfMIHWBgNVHSMEgc4wgcuAFLm33UrNww4Mhp1d3+wcBGnFTpjf
18
+ oYGnpIGkMIGhMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8G
19
+ A1UEBxMIU2FuIEpvc2UxFTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMM
20
+ WnNjYWxlciBJbmMuMRgwFgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG
21
+ 9w0BCQEWE3N1cHBvcnRAenNjYWxlci5jb22CCQDbvpgtibd7kzAMBgNVHRMEBTAD
22
+ AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw0NdJh8w3NsJu4KHuVZUrmZgIohnTm0j+
23
+ RTmYQ9IKA/pvxAcA6K1i/LO+Bt+tCX+C0yxqB8qzuo+4vAzoY5JEBhyhBhf1uK+P
24
+ /WVWFZN/+hTgpSbZgzUEnWQG2gOVd24msex+0Sr7hyr9vn6OueH+jj+vCMiAm5+u
25
+ kd7lLvJsBu3AO3jGWVLyPkS3i6Gf+rwAp1OsRrv3WnbkYcFf9xjuaf4z0hRCrLN2
26
+ xFNjavxrHmsH8jPHVvgc1VD0Opja0l/BRVauTrUaoW6tE+wFG5rEcPGS80jjHK4S
27
+ pB5iDj2mUZH1T8lzYtuZy0ZPirxmtsk3135+CKNa2OCAhhFjE0xd
28
+ -----END CERTIFICATE-----
scripts/test.sh CHANGED
@@ -15,9 +15,16 @@ touch .env
15
15
  # Add the project root to PYTHONPATH so imports work correctly
16
16
  export PYTHONPATH="${PROJECT_DIR}:${PYTHONPATH:-}"
17
17
 
18
- # Run all tests using pytest with coverage
19
- echo "Running tests with coverage..."
20
- pytest tests/ -v --cov=aixtools --cov-report=term-missing
21
-
22
- # Generate HTML coverage report
23
- # pytest tests/ --cov=aixtools --cov-report=html
18
+ # Check if a specific test is provided as parameter
19
+ if [ $# -eq 1 ]; then
20
+ # Run specific test without coverage
21
+ echo "Running specific test: $1"
22
+ pytest "$1" -v
23
+ else
24
+ # Run all tests using pytest with coverage
25
+ echo "Running tests with coverage..."
26
+ pytest tests/ -v --cov=aixtools --cov-report=term-missing
27
+
28
+ # Generate HTML coverage report
29
+ # pytest tests/ --cov=aixtools --cov-report=html
30
+ fi
@@ -0,0 +1,329 @@
1
+ import json
2
+ import unittest
3
+ from pathlib import Path
4
+
5
+ from aixtools.compliance.private_data import PrivateData, PRIVATE_DATA_FILE
6
+ from aixtools.server.path import get_workspace_path
7
+
8
+
9
+ class TestPrivateData(unittest.TestCase):
10
+ """Test cases for PrivateData class without mocking or patching."""
11
+
12
+ def setUp(self):
13
+ """Set up test environment with temporary directory."""
14
+ # Create a test user and session context
15
+ self.workspace_path = Path(get_workspace_path())
16
+ self.workspace_path.mkdir(parents=True, exist_ok=True)
17
+ self.private_data_file = self.workspace_path / PRIVATE_DATA_FILE
18
+
19
+ def tearDown(self):
20
+ """Clean up test environment."""
21
+ self.private_data_file.unlink(missing_ok=True)
22
+
23
+ def test_init_no_existing_file(self):
24
+ """Test initialization when no private data file exists."""
25
+ private_data = PrivateData()
26
+ self.assertFalse(private_data.has_private_data)
27
+ self.assertEqual(private_data.get_private_datasets(), [])
28
+ self.assertEqual(private_data.get_idap_datasets(), [])
29
+
30
+ def test_init_with_existing_file(self):
31
+ """Test initialization when private data file exists."""
32
+ # Create a test file
33
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
34
+ test_data = {
35
+ "_has_private_data": True,
36
+ "_private_datasets": ["dataset1", "dataset2"],
37
+ "_idap_datasets": ["dataset1"],
38
+ "ctx": None
39
+ }
40
+ with open(private_data_path, "w") as f:
41
+ json.dump(test_data, f)
42
+
43
+ private_data = PrivateData()
44
+
45
+ self.assertTrue(private_data.has_private_data)
46
+ self.assertEqual(set(private_data.get_private_datasets()), {"dataset1", "dataset2"})
47
+ self.assertEqual(private_data.get_idap_datasets(), ["dataset1"])
48
+
49
+ def test_add_private_dataset_new(self):
50
+ """Test adding a new private dataset."""
51
+ private_data = PrivateData()
52
+
53
+ private_data.add_private_dataset("test_dataset")
54
+
55
+ self.assertTrue(private_data.has_private_data)
56
+ self.assertIn("test_dataset", private_data.get_private_datasets())
57
+
58
+ # Verify data is saved to file
59
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
60
+ self.assertTrue(private_data_path.exists())
61
+
62
+ with open(private_data_path, "r") as f:
63
+ saved_data = json.load(f)
64
+
65
+ self.assertTrue(saved_data["_has_private_data"])
66
+ self.assertIn("test_dataset", saved_data["_private_datasets"])
67
+
68
+ def test_add_private_dataset_duplicate(self):
69
+ """Test adding a duplicate private dataset."""
70
+ private_data = PrivateData()
71
+
72
+ private_data.add_private_dataset("test_dataset")
73
+ initial_count = len(private_data.get_private_datasets())
74
+
75
+ private_data.add_private_dataset("test_dataset")
76
+ final_count = len(private_data.get_private_datasets())
77
+
78
+ self.assertEqual(initial_count, final_count)
79
+ self.assertEqual(private_data.get_private_datasets().count("test_dataset"), 1)
80
+
81
+ def test_add_idap_dataset_new(self):
82
+ """Test adding a new IDAP dataset."""
83
+ private_data = PrivateData()
84
+
85
+ private_data.add_idap_dataset("idap_dataset")
86
+
87
+ self.assertTrue(private_data.has_private_data)
88
+ self.assertIn("idap_dataset", private_data.get_idap_datasets())
89
+ self.assertIn("idap_dataset", private_data.get_private_datasets())
90
+
91
+ # Verify data is saved to file
92
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
93
+ self.assertTrue(private_data_path.exists())
94
+
95
+ with open(private_data_path, "r") as f:
96
+ saved_data = json.load(f)
97
+
98
+ self.assertTrue(saved_data["_has_private_data"])
99
+ self.assertIn("idap_dataset", saved_data["_idap_datasets"])
100
+ self.assertIn("idap_dataset", saved_data["_private_datasets"])
101
+
102
+ def test_add_idap_dataset_existing_private(self):
103
+ """Test adding IDAP dataset when it already exists as private dataset."""
104
+ private_data = PrivateData()
105
+
106
+ private_data.add_private_dataset("existing_dataset")
107
+ private_data.add_idap_dataset("existing_dataset")
108
+
109
+ self.assertTrue(private_data.has_private_data)
110
+ self.assertIn("existing_dataset", private_data.get_idap_datasets())
111
+ self.assertIn("existing_dataset", private_data.get_private_datasets())
112
+ self.assertEqual(private_data.get_private_datasets().count("existing_dataset"), 1)
113
+
114
+ def test_add_idap_dataset_duplicate(self):
115
+ """Test adding a duplicate IDAP dataset."""
116
+ private_data = PrivateData()
117
+
118
+ private_data.add_idap_dataset("idap_dataset")
119
+ initial_idap_count = len(private_data.get_idap_datasets())
120
+ initial_private_count = len(private_data.get_private_datasets())
121
+
122
+ private_data.add_idap_dataset("idap_dataset")
123
+ final_idap_count = len(private_data.get_idap_datasets())
124
+ final_private_count = len(private_data.get_private_datasets())
125
+
126
+ self.assertEqual(initial_idap_count, final_idap_count)
127
+ self.assertEqual(initial_private_count, final_private_count)
128
+
129
+ def test_has_private_dataset(self):
130
+ """Test checking if a private dataset exists."""
131
+ private_data = PrivateData()
132
+
133
+ self.assertFalse(private_data.has_private_dataset("nonexistent"))
134
+
135
+ private_data.add_private_dataset("test_dataset")
136
+
137
+ self.assertTrue(private_data.has_private_dataset("test_dataset"))
138
+ self.assertFalse(private_data.has_private_dataset("nonexistent"))
139
+
140
+ def test_has_idap_dataset(self):
141
+ """Test checking if an IDAP dataset exists."""
142
+ private_data = PrivateData()
143
+
144
+ self.assertFalse(private_data.has_idap_dataset("nonexistent"))
145
+
146
+ private_data.add_idap_dataset("idap_dataset")
147
+
148
+ self.assertTrue(private_data.has_idap_dataset("idap_dataset"))
149
+ self.assertFalse(private_data.has_idap_dataset("nonexistent"))
150
+
151
+ def test_get_datasets_returns_copies(self):
152
+ """Test that get methods return copies to prevent external modification."""
153
+ private_data = PrivateData()
154
+ private_data.add_private_dataset("private_dataset")
155
+ private_data.add_idap_dataset("idap_dataset")
156
+
157
+ private_datasets = private_data.get_private_datasets()
158
+ idap_datasets = private_data.get_idap_datasets()
159
+
160
+ # Modify the returned lists
161
+ private_datasets.append("external_modification")
162
+ idap_datasets.append("external_modification")
163
+
164
+ # Verify original data is unchanged
165
+ self.assertNotIn("external_modification", private_data.get_private_datasets())
166
+ self.assertNotIn("external_modification", private_data.get_idap_datasets())
167
+
168
+ def test_has_private_data_setter_true(self):
169
+ """Test setting has_private_data to True."""
170
+ private_data = PrivateData()
171
+
172
+ private_data.has_private_data = True
173
+
174
+ self.assertTrue(private_data.has_private_data)
175
+
176
+ # Verify data is saved to file
177
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
178
+ self.assertTrue(private_data_path.exists())
179
+
180
+ def test_has_private_data_setter_false(self):
181
+ """Test setting has_private_data to False clears all data."""
182
+ private_data = PrivateData()
183
+ private_data.add_private_dataset("dataset1")
184
+ private_data.add_idap_dataset("dataset2")
185
+
186
+ private_data.has_private_data = False
187
+
188
+ self.assertFalse(private_data.has_private_data)
189
+ self.assertEqual(private_data.get_private_datasets(), [])
190
+ self.assertEqual(private_data.get_idap_datasets(), [])
191
+
192
+ # Verify file is deleted
193
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
194
+ self.assertFalse(private_data_path.exists())
195
+
196
+ def test_save_with_no_private_data(self):
197
+ """Test saving when has_private_data is False deletes the file."""
198
+ private_data = PrivateData()
199
+ private_data.add_private_dataset("test_dataset")
200
+
201
+ # Verify file exists
202
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
203
+ self.assertTrue(private_data_path.exists())
204
+
205
+ private_data.has_private_data = False
206
+
207
+ # Verify file is deleted
208
+ self.assertFalse(private_data_path.exists())
209
+
210
+ def test_save_with_private_data(self):
211
+ """Test saving when has_private_data is True creates/updates the file."""
212
+ private_data = PrivateData()
213
+ private_data.add_private_dataset("dataset1")
214
+ private_data.add_idap_dataset("dataset2")
215
+
216
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
217
+ self.assertTrue(private_data_path.exists())
218
+
219
+ with open(private_data_path, "r") as f:
220
+ saved_data = json.load(f)
221
+
222
+ self.assertTrue(saved_data["_has_private_data"])
223
+ self.assertIn("dataset1", saved_data["_private_datasets"])
224
+ self.assertIn("dataset2", saved_data["_private_datasets"])
225
+ self.assertIn("dataset2", saved_data["_idap_datasets"])
226
+ self.assertIsNone(saved_data["ctx"])
227
+
228
+ def test_load_from_corrupted_file(self):
229
+ """Test loading from a corrupted JSON file."""
230
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
231
+ with open(private_data_path, "w") as f:
232
+ f.write("invalid json content")
233
+
234
+ with self.assertRaises(json.JSONDecodeError):
235
+ PrivateData()
236
+
237
+ def test_load_from_file_with_missing_fields(self):
238
+ """Test loading from a file with missing fields uses defaults."""
239
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
240
+ test_data = {"_has_private_data": True} # Missing other fields
241
+ with open(private_data_path, "w") as f:
242
+ json.dump(test_data, f)
243
+
244
+ private_data = PrivateData()
245
+
246
+ self.assertTrue(private_data.has_private_data)
247
+ self.assertEqual(private_data.get_private_datasets(), [])
248
+ self.assertEqual(private_data.get_idap_datasets(), [])
249
+
250
+ def test_multiple_operations_persistence(self):
251
+ """Test that multiple operations are properly persisted."""
252
+ # Create first instance and add data
253
+ private_data1 = PrivateData()
254
+ private_data1.add_private_dataset("dataset1")
255
+ private_data1.add_idap_dataset("dataset2")
256
+
257
+ # Create second instance and verify data is loaded
258
+ private_data2 = PrivateData()
259
+ self.assertTrue(private_data2.has_private_data)
260
+ self.assertIn("dataset1", private_data2.get_private_datasets())
261
+ self.assertIn("dataset2", private_data2.get_private_datasets())
262
+ self.assertIn("dataset2", private_data2.get_idap_datasets())
263
+
264
+ # Add more data with second instance
265
+ private_data2.add_private_dataset("dataset3")
266
+
267
+ # Create third instance and verify all data is present
268
+ private_data3 = PrivateData()
269
+ expected_private = {"dataset1", "dataset2", "dataset3"}
270
+ expected_idap = {"dataset2"}
271
+
272
+ self.assertEqual(set(private_data3.get_private_datasets()), expected_private)
273
+ self.assertEqual(set(private_data3.get_idap_datasets()), expected_idap)
274
+
275
+ def test_str_and_repr(self):
276
+ """Test string representation methods."""
277
+ private_data = PrivateData()
278
+ private_data.add_private_dataset("dataset1")
279
+ private_data.add_idap_dataset("dataset2")
280
+
281
+ str_repr = str(private_data)
282
+ repr_repr = repr(private_data)
283
+
284
+ self.assertEqual(str_repr, repr_repr)
285
+ self.assertIn("has_private_data=True", str_repr)
286
+ self.assertIn("dataset1", str_repr)
287
+ self.assertIn("dataset2", str_repr)
288
+ self.assertIn(str(self.workspace_path / PRIVATE_DATA_FILE), str_repr)
289
+
290
+ def test_file_operations_create_directories(self):
291
+ """Test that file operations create necessary directories."""
292
+ # Remove the workspace directory
293
+ import shutil
294
+ shutil.rmtree(self.workspace_path)
295
+ self.assertFalse(self.workspace_path.exists())
296
+
297
+ # Create PrivateData instance and add data
298
+ private_data = PrivateData()
299
+ private_data.add_private_dataset("test_dataset")
300
+
301
+ # Verify directory and file were created
302
+ self.assertTrue(self.workspace_path.exists())
303
+ private_data_path = self.workspace_path / PRIVATE_DATA_FILE
304
+ self.assertTrue(private_data_path.exists())
305
+
306
+ def test_concurrent_modifications_simulation(self):
307
+ """Test simulation of concurrent modifications (without actual threading)."""
308
+ # Create two instances with same context
309
+ private_data1 = PrivateData()
310
+ private_data2 = PrivateData()
311
+
312
+ # Add data with first instance
313
+ private_data1.add_private_dataset("dataset1")
314
+
315
+ # Second instance should see the changes when reloaded
316
+ private_data2.load()
317
+ self.assertIn("dataset1", private_data2.get_private_datasets())
318
+
319
+ # Add data with second instance
320
+ private_data2.add_idap_dataset("dataset2")
321
+
322
+ # First instance should see the changes when reloaded
323
+ private_data1.load()
324
+ self.assertIn("dataset2", private_data1.get_idap_datasets())
325
+ self.assertIn("dataset2", private_data1.get_private_datasets())
326
+
327
+
328
+ if __name__ == "__main__":
329
+ unittest.main()
@@ -159,6 +159,81 @@ def test_read_user_service_secret_not_found(patched_vault_client):
159
159
  )
160
160
 
161
161
 
162
+ def test_list_user_secret_keys_success(patched_vault_client):
163
+ """Test successful listing of all user secret keys."""
164
+ mock_response = {"data": {"keys": ["service1/", "service2/", "service3"]}}
165
+ patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = mock_response
166
+
167
+ result = patched_vault_client.list_user_secret_keys(user_id="test-user")
168
+
169
+ assert result == ["service1", "service2", "service3"]
170
+ patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
171
+ path="path/dev/test-user", mount_point="secret"
172
+ )
173
+
174
+
175
+ def test_list_user_secret_keys_no_secrets(patched_vault_client):
176
+ """Test listing user secret keys when no secrets exist."""
177
+ mock_response = {"data": {"keys": []}}
178
+ patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = mock_response
179
+
180
+ result = patched_vault_client.list_user_secret_keys(user_id="test-user")
181
+
182
+ assert result == []
183
+ patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
184
+ path="path/dev/test-user", mount_point="secret"
185
+ )
186
+
187
+
188
+ def test_list_user_secret_keys_empty_response(patched_vault_client):
189
+ """Test listing user secret keys when response is empty."""
190
+ patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = {}
191
+
192
+ result = patched_vault_client.list_user_secret_keys(user_id="test-user")
193
+
194
+ assert result == []
195
+ patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
196
+ path="path/dev/test-user", mount_point="secret"
197
+ )
198
+
199
+
200
+ def test_list_user_secret_keys_invalid_path(patched_vault_client):
201
+ """Test listing user secret keys when user path does not exist."""
202
+ patched_vault_client.client.secrets.kv.v2.list_secrets.side_effect = InvalidPath
203
+
204
+ result = patched_vault_client.list_user_secret_keys(user_id="test-user")
205
+
206
+ assert result == []
207
+ patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
208
+ path="path/dev/test-user", mount_point="secret"
209
+ )
210
+
211
+
212
+ def test_list_user_secret_keys_unexpected_error(patched_vault_client):
213
+ """Test listing user secret keys when an unexpected exception occurs."""
214
+ patched_vault_client.client.secrets.kv.v2.list_secrets.side_effect = Exception("List error")
215
+
216
+ with pytest.raises(VaultAuthError, match="List error"):
217
+ patched_vault_client.list_user_secret_keys(user_id="test-user")
218
+
219
+ patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
220
+ path="path/dev/test-user", mount_point="secret"
221
+ )
222
+
223
+
224
+ def test_list_user_secret_keys_strips_trailing_slashes(patched_vault_client):
225
+ """Test that trailing slashes are properly stripped from directory names."""
226
+ mock_response = {"data": {"keys": ["service1/", "service2/", "service3", "service4/"]}}
227
+ patched_vault_client.client.secrets.kv.v2.list_secrets.return_value = mock_response
228
+
229
+ result = patched_vault_client.list_user_secret_keys(user_id="test-user")
230
+
231
+ assert result == ["service1", "service2", "service3", "service4"]
232
+ patched_vault_client.client.secrets.kv.v2.list_secrets.assert_called_once_with(
233
+ path="path/dev/test-user", mount_point="secret"
234
+ )
235
+
236
+
162
237
  def test_read_user_service_secret_error(patched_vault_client):
163
238
  """Test read_user_service_secret when an unexpected exception occurs."""
164
239
  patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = Exception("Read error")