gcp-platforms-auto 0.7.0__py3-none-any.whl → 0.7.5__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.
@@ -1 +1,15 @@
1
- from .git import get_github_app_token, generate_github_path, get_repo, git_push, create_repo
1
+ from .git import get_github_app_token, generate_github_path, get_repo, git_push, create_repo
2
+ from .db import (
3
+ get_sql_engine,
4
+ execute_query,
5
+ cleanup_connector,
6
+ create_tables,
7
+ save_model,
8
+ get_all,
9
+ get_by_id,
10
+ get_by_filter,
11
+ get_one,
12
+ update_model,
13
+ delete_model,
14
+ Base
15
+ )
@@ -0,0 +1,277 @@
1
+ import os
2
+ import logging
3
+ import sqlalchemy
4
+ from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
5
+ from sqlalchemy import text
6
+
7
+ # Configure the logger
8
+ logger = logging.getLogger("uvicorn")
9
+ logger.setLevel(logging.INFO)
10
+
11
+ Base = declarative_base()
12
+
13
+ def get_sql_engine(db_host, db_user, db_pass, db_name, db_port=5432) -> sqlalchemy.engine.base.Engine:
14
+ """Initializes a TCP connection pool for a Cloud SQL instance of Postgres.
15
+
16
+ Args:
17
+ db_host: Database host address (e.g., '127.0.0.1')
18
+ db_user: Database username
19
+ db_pass: Database password
20
+ db_name: Database name
21
+ db_port: Database port (default: 5432)
22
+
23
+ Returns:
24
+ SQLAlchemy Engine instance
25
+ """
26
+ logger.info(f"Connecting to database '{db_name}' at {db_host}:{db_port}")
27
+
28
+ try:
29
+ pool = sqlalchemy.create_engine(
30
+ # Equivalent URL:
31
+ # postgresql+pg8000://<db_user>:<db_pass>@<db_host>:<db_port>/<db_name>
32
+ sqlalchemy.engine.url.URL.create(
33
+ drivername="postgresql+pg8000",
34
+ username=db_user,
35
+ password=db_pass,
36
+ host=db_host,
37
+ port=db_port,
38
+ database=db_name,
39
+ ),
40
+ )
41
+ logger.info("Successfully created database connection pool.")
42
+ return pool
43
+ except Exception as e:
44
+ logger.exception(f"Error connecting to database: {str(e)}")
45
+ raise e
46
+
47
+ def get_session(engine):
48
+ """Creates and returns a new session for the provided engine.
49
+
50
+ Args:
51
+ engine: SQLAlchemy Engine instance
52
+
53
+ Returns:
54
+ SQLAlchemy Session instance
55
+ """
56
+ Session = sessionmaker(bind=engine)
57
+ return Session()
58
+
59
+ def execute_query(engine, query_str: str, params: dict = None):
60
+ """Executes a raw SQL query.
61
+
62
+ Args:
63
+ engine: SQLAlchemy Engine instance
64
+ query_str: SQL query string
65
+ params: Dictionary of query parameters (optional)
66
+
67
+ Returns:
68
+ Query result
69
+ """
70
+ session = get_session(engine)
71
+ try:
72
+ logger.info(f"Executing query: {query_str[:100]}...")
73
+ result = session.execute(text(query_str), params or {})
74
+ session.commit()
75
+ logger.info("Query executed successfully.")
76
+ return result
77
+ except Exception as e:
78
+ session.rollback()
79
+ logger.exception(f"Error executing query: {str(e)}")
80
+ raise e
81
+ finally:
82
+ session.close()
83
+
84
+ def create_tables(engine, base_class=Base):
85
+ """Creates all tables defined in the SQLAlchemy Base metadata.
86
+
87
+ Args:
88
+ engine: SQLAlchemy Engine instance
89
+ base_class: SQLAlchemy declarative base class (default: Base)
90
+ """
91
+ try:
92
+ logger.info("Creating database tables...")
93
+ base_class.metadata.create_all(engine)
94
+ logger.info("Successfully created database tables.")
95
+ except Exception as e:
96
+ logger.exception(f"Error creating tables: {str(e)}")
97
+ raise e
98
+
99
+ def save_model(engine, model_instance):
100
+ """Saves a single model instance to the database.
101
+
102
+ Args:
103
+ engine: SQLAlchemy Engine instance
104
+ model_instance: SQLAlchemy model instance to save
105
+
106
+ Returns:
107
+ The saved model instance with refreshed data
108
+ """
109
+ session = get_session(engine)
110
+ try:
111
+ logger.info(f"Saving model instance: {type(model_instance).__name__}")
112
+ session.add(model_instance)
113
+ session.commit()
114
+ session.refresh(model_instance)
115
+ logger.info("Successfully saved model instance.")
116
+ return model_instance
117
+ except Exception as e:
118
+ session.rollback()
119
+ logger.exception(f"Error saving model: {str(e)}")
120
+ raise e
121
+ finally:
122
+ session.close()
123
+
124
+ def get_all(engine, model_class):
125
+ """Retrieves all records for a given model class.
126
+
127
+ Args:
128
+ engine: SQLAlchemy Engine instance
129
+ model_class: SQLAlchemy model class
130
+
131
+ Returns:
132
+ List of all records for the model
133
+ """
134
+ session = get_session(engine)
135
+ try:
136
+ logger.info(f"Retrieving all records for {model_class.__name__}")
137
+ results = session.query(model_class).all()
138
+ logger.info(f"Retrieved {len(results)} records.")
139
+ return results
140
+ except Exception as e:
141
+ logger.exception(f"Error retrieving records: {str(e)}")
142
+ raise e
143
+ finally:
144
+ session.close()
145
+
146
+ def get_by_id(engine, model_class, record_id):
147
+ """Retrieves a single record by its primary key ID.
148
+
149
+ Args:
150
+ engine: SQLAlchemy Engine instance
151
+ model_class: SQLAlchemy model class
152
+ record_id: Primary key value to search for
153
+
154
+ Returns:
155
+ Model instance if found, None otherwise
156
+ """
157
+ session = get_session(engine)
158
+ try:
159
+ logger.info(f"Retrieving {model_class.__name__} with ID: {record_id}")
160
+ result = session.query(model_class).get(record_id)
161
+ if result:
162
+ logger.info(f"Found record with ID: {record_id}")
163
+ else:
164
+ logger.info(f"No record found with ID: {record_id}")
165
+ return result
166
+ except Exception as e:
167
+ logger.exception(f"Error retrieving record by ID: {str(e)}")
168
+ raise e
169
+ finally:
170
+ session.close()
171
+
172
+ def get_by_filter(engine, model_class, **filters):
173
+ """Retrieves records matching the provided filter criteria.
174
+
175
+ Args:
176
+ engine: SQLAlchemy Engine instance
177
+ model_class: SQLAlchemy model class
178
+ **filters: Keyword arguments for filtering (e.g., name="John", age=30)
179
+
180
+ Returns:
181
+ List of matching records
182
+ """
183
+ session = get_session(engine)
184
+ try:
185
+ logger.info(f"Filtering {model_class.__name__} with criteria: {filters}")
186
+ results = session.query(model_class).filter_by(**filters).all()
187
+ logger.info(f"Found {len(results)} matching records.")
188
+ return results
189
+ except Exception as e:
190
+ logger.exception(f"Error filtering records: {str(e)}")
191
+ raise e
192
+ finally:
193
+ session.close()
194
+
195
+ def get_one(engine, model_class, **filters):
196
+ """Retrieves a single record matching the provided filter criteria.
197
+
198
+ Args:
199
+ engine: SQLAlchemy Engine instance
200
+ model_class: SQLAlchemy model class
201
+ **filters: Keyword arguments for filtering (e.g., email="user@example.com")
202
+
203
+ Returns:
204
+ First matching model instance if found, None otherwise
205
+ """
206
+ session = get_session(engine)
207
+ try:
208
+ logger.info(f"Retrieving single {model_class.__name__} with criteria: {filters}")
209
+ result = session.query(model_class).filter_by(**filters).first()
210
+ if result:
211
+ logger.info(f"Found matching record.")
212
+ else:
213
+ logger.info(f"No matching record found.")
214
+ return result
215
+ except Exception as e:
216
+ logger.exception(f"Error retrieving single record: {str(e)}")
217
+ raise e
218
+ finally:
219
+ session.close()
220
+
221
+ def update_model(engine, model_instance):
222
+ """Updates an existing model instance in the database.
223
+
224
+ Args:
225
+ engine: SQLAlchemy Engine instance
226
+ model_instance: SQLAlchemy model instance with updated values
227
+
228
+ Returns:
229
+ The updated model instance with refreshed data
230
+ """
231
+ session = get_session(engine)
232
+ try:
233
+ logger.info(f"Updating model instance: {type(model_instance).__name__}")
234
+ session.merge(model_instance)
235
+ session.commit()
236
+ logger.info("Successfully updated model instance.")
237
+ return model_instance
238
+ except Exception as e:
239
+ session.rollback()
240
+ logger.exception(f"Error updating model: {str(e)}")
241
+ raise e
242
+ finally:
243
+ session.close()
244
+
245
+ def delete_model(engine, model_instance):
246
+ """Deletes a model instance from the database.
247
+
248
+ Args:
249
+ engine: SQLAlchemy Engine instance
250
+ model_instance: SQLAlchemy model instance to delete
251
+ """
252
+ session = get_session(engine)
253
+ try:
254
+ logger.info(f"Deleting model instance: {type(model_instance).__name__}")
255
+ session.delete(model_instance)
256
+ session.commit()
257
+ logger.info("Successfully deleted model instance.")
258
+ except Exception as e:
259
+ session.rollback()
260
+ logger.exception(f"Error deleting model: {str(e)}")
261
+ raise e
262
+ finally:
263
+ session.close()
264
+
265
+ def cleanup_connector(engine):
266
+ """Disposes the connection pool for the provided engine.
267
+
268
+ Args:
269
+ engine: SQLAlchemy Engine instance to dispose
270
+ """
271
+ try:
272
+ logger.info("Disposing database connection pool...")
273
+ engine.dispose()
274
+ logger.info("Successfully disposed connection pool.")
275
+ except Exception as e:
276
+ logger.exception(f"Error disposing connection pool: {str(e)}")
277
+ raise e
@@ -0,0 +1,212 @@
1
+ """IAM access management utilities for GCP."""
2
+
3
+ import logging
4
+ import google.cloud.logging
5
+ from google.cloud import asset_v1, resourcemanager_v3
6
+ from typing import Optional
7
+
8
+ # Initialize Google Cloud Logging
9
+ client = google.cloud.logging.Client()
10
+ client.setup_logging()
11
+
12
+ # Configure the logger
13
+ logger = logging.getLogger("uvicorn")
14
+ logger.setLevel(logging.INFO)
15
+
16
+
17
+ def _get_identity_string(email: str) -> str:
18
+ """
19
+ Helper function to construct the proper identity string.
20
+ Automatically detects if the email is a service account or user.
21
+
22
+ Args:
23
+ email: Email address (user or service account)
24
+
25
+ Returns:
26
+ str: Properly formatted identity string
27
+ """
28
+ if ".gserviceaccount.com" in email.lower():
29
+ return f"serviceAccount:{email}"
30
+ else:
31
+ return f"user:{email}"
32
+
33
+
34
+ def check_user_has_role(
35
+ project_id: str,
36
+ user_email: str,
37
+ organization_id: str,
38
+ role: str = "roles/owner",
39
+ expand_groups: bool = True
40
+ ) -> bool:
41
+ """
42
+ Check if a user or service account has a specific role in a GCP project.
43
+
44
+ Args:
45
+ user_email: Email of the user or service account to check
46
+ (e.g., 'oshasha10@dev.sky320.com' or 'my-sa@project.iam.gserviceaccount.com')
47
+ role: Role to check (e.g., 'roles/owner', 'roles/editor')
48
+ project_id: GCP project ID (e.g., 'sky-starfi-mam-res-gcpro-1')
49
+ organization_id: GCP organization ID (e.g., '111111111111')
50
+ expand_groups: Whether to expand group memberships (default: True)
51
+
52
+ Returns:
53
+ bool: True if the user/service account has the role in the project, False otherwise
54
+
55
+ Example:
56
+ >>> has_access = check_user_has_role(
57
+ ... user_email='oshasha10@dev.sky320.com',
58
+ ... role='roles/owner',
59
+ ... project_id='sky-starfi-mam-res-gcpro-1'
60
+ ... )
61
+ """
62
+ client = asset_v1.AssetServiceClient()
63
+
64
+ # Construct the full resource name
65
+ scope = f"organizations/{organization_id}"
66
+ full_resource_name = f"//cloudresourcemanager.googleapis.com/projects/{project_id}"
67
+ identity = _get_identity_string(user_email)
68
+
69
+ # Build the request
70
+ request = asset_v1.AnalyzeIamPolicyRequest(
71
+ analysis_query=asset_v1.IamPolicyAnalysisQuery(
72
+ scope=scope,
73
+ resource_selector=asset_v1.IamPolicyAnalysisQuery.ResourceSelector(
74
+ full_resource_name=full_resource_name
75
+ ),
76
+ identity_selector=asset_v1.IamPolicyAnalysisQuery.IdentitySelector(
77
+ identity=identity
78
+ ),
79
+ access_selector=asset_v1.IamPolicyAnalysisQuery.AccessSelector(
80
+ roles=[role]
81
+ ),
82
+ options=asset_v1.IamPolicyAnalysisQuery.Options(
83
+ expand_groups=expand_groups,
84
+ expand_roles=True,
85
+ # expand_resources=True
86
+ )
87
+ )
88
+ )
89
+
90
+ try:
91
+ # Execute the analysis
92
+ logger.info(f"Checking if {user_email} has role {role} in project {project_id}")
93
+ response = client.analyze_iam_policy(request=request)
94
+
95
+ # Check if any results were returned
96
+ # If the user has the role, there will be analysis results
97
+ if response.main_analysis and response.main_analysis.analysis_results:
98
+ logger.info(f"{user_email} has role {role} in project {project_id}")
99
+ return True
100
+
101
+ logger.info(f"{user_email} does not have role {role} in project {project_id}")
102
+ return False
103
+
104
+ except Exception as e:
105
+ logger.exception(f"Error analyzing IAM policy: {e}")
106
+ raise
107
+
108
+
109
+ def _get_all_folders_recursive(parent: str, folders_client) -> list:
110
+ """
111
+ Recursively get all folders under a parent (organization or folder).
112
+
113
+ Args:
114
+ parent: Parent resource (e.g., 'organizations/123' or 'folders/456')
115
+ folders_client: FoldersClient instance
116
+
117
+ Returns:
118
+ list: List of all folder resource names
119
+ """
120
+ all_folders = []
121
+
122
+ try:
123
+ request = resourcemanager_v3.ListFoldersRequest(parent=parent)
124
+ folders = folders_client.list_folders(request=request)
125
+
126
+ for folder in folders:
127
+ folder_name = folder.name # Format: folders/123
128
+ all_folders.append(folder_name)
129
+ logger.info(f"Found folder: {folder_name}")
130
+
131
+ # Recursively get subfolders
132
+ subfolders = _get_all_folders_recursive(folder_name, folders_client)
133
+ all_folders.extend(subfolders)
134
+
135
+ except Exception as e:
136
+ logger.warning(f"Error listing folders under {parent}: {e}")
137
+
138
+ return all_folders
139
+
140
+
141
+ def get_projects_with_role(
142
+ user_email: str,
143
+ organization_id: str,
144
+ role: str = "roles/owner",
145
+ expand_groups: bool = True
146
+ ) -> list:
147
+ """
148
+ Get all projects where a user or service account has a specific role.
149
+ Searches recursively through all folders in the organization.
150
+
151
+ Args:
152
+ user_email: Email of the user or service account to check
153
+ (e.g., 'oshasha10@dev.sky320.com' or 'my-sa@project.iam.gserviceaccount.com')
154
+ organization_id: GCP organization ID (e.g., '111111111111')
155
+ role: Role to check (e.g., 'roles/owner', 'roles/editor')
156
+ expand_groups: Whether to expand group memberships (default: True)
157
+
158
+ Returns:
159
+ list: List of project IDs where the user/service account has the specified role
160
+
161
+ Example:
162
+ >>> projects = get_projects_with_role(
163
+ ... user_email='oshasha10@dev.sky320.com',
164
+ ... organization_id='111111111111',
165
+ ... role='roles/owner'
166
+ ... )
167
+ """
168
+ logger.info(f"Fetching all projects in organization {organization_id} (including folders)")
169
+
170
+ try:
171
+ # Initialize clients
172
+ projects_client = resourcemanager_v3.ProjectsClient()
173
+ folders_client = resourcemanager_v3.FoldersClient()
174
+
175
+ # Get all folders recursively
176
+ org_parent = f"organizations/{organization_id}"
177
+ all_folders = _get_all_folders_recursive(org_parent, folders_client)
178
+ logger.info(f"Found {len(all_folders)} folder(s) in organization")
179
+
180
+ # Create list of all parents to search (organization + all folders)
181
+ parents_to_search = [org_parent] + all_folders
182
+
183
+ # Collect all projects from all parents
184
+ all_projects = []
185
+ for parent in parents_to_search:
186
+ request = resourcemanager_v3.ListProjectsRequest(parent=parent)
187
+ projects = projects_client.list_projects(request=request)
188
+ for project in projects:
189
+ all_projects.append(project.project_id)
190
+
191
+ logger.info(f"Found {len(all_projects)} total project(s) across organization and folders")
192
+
193
+ # Filter projects where user has the specified role
194
+ matching_projects = []
195
+
196
+ for project_id in all_projects:
197
+ # Check if user has the role in this project
198
+ if check_user_has_role(
199
+ project_id=project_id,
200
+ user_email=user_email,
201
+ organization_id=organization_id,
202
+ role=role,
203
+ expand_groups=expand_groups
204
+ ):
205
+ matching_projects.append(project_id)
206
+
207
+ logger.info(f"Checked {len(all_projects)} projects, found {len(matching_projects)} where {user_email} has role {role}")
208
+ return sorted(matching_projects)
209
+
210
+ except Exception as e:
211
+ logger.exception(f"Error fetching projects: {e}")
212
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gcp_platforms_auto
3
- Version: 0.7.0
3
+ Version: 0.7.5
4
4
  Summary: A brief description of your package
5
5
  Author-email: ofir4858 <ofirshasha10@gmail.com>
6
6
  License: MIT
@@ -12,6 +12,10 @@ Description-Content-Type: text/markdown
12
12
  Requires-Dist: requests
13
13
  Requires-Dist: pyjwt
14
14
  Requires-Dist: google-cloud-logging
15
+ Requires-Dist: google-cloud-asset
16
+ Requires-Dist: google-cloud-resource-manager
15
17
  Requires-Dist: gitpython
18
+ Requires-Dist: sqlalchemy
19
+ Requires-Dist: pg8000
16
20
 
17
21
  # gcp_sdk
@@ -0,0 +1,8 @@
1
+ gcp_platforms_auto/__init__.py,sha256=jpdcmFArf_rwuQ0Cn26GUq5_DxSbGF6oMwxSKYu7ELY,322
2
+ gcp_platforms_auto/db.py,sha256=jE5nwmqVHcxT4m6-meUgUz4V4DM8M_sMmeTpvKr2Z2Y,8768
3
+ gcp_platforms_auto/git.py,sha256=NnLDfRzzrzbm9yekepc-qgu8ejYmjNxQ4VDlW46gG2o,5508
4
+ gcp_platforms_auto/iam.py,sha256=h3NytTLB04eO6TvxVwBN2vWJOElGQaEkWeoNkRD6BVI,7653
5
+ gcp_platforms_auto-0.7.5.dist-info/METADATA,sha256=IJDfoM9XSyYJlKAk7dEGE1o7noOIDBpF6p0j5rAoFfo,621
6
+ gcp_platforms_auto-0.7.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
7
+ gcp_platforms_auto-0.7.5.dist-info/top_level.txt,sha256=4q-ofPMmvBaTnIbAzs-Wp_OwheAVxxmJ1fW9vl3-kyE,19
8
+ gcp_platforms_auto-0.7.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- gcp_platforms_auto/__init__.py,sha256=7CmPCQxUqAvZTHKCREhL2b4MKD8Pn7FnKZUcMxYC09g,92
2
- gcp_platforms_auto/git.py,sha256=NnLDfRzzrzbm9yekepc-qgu8ejYmjNxQ4VDlW46gG2o,5508
3
- gcp_platforms_auto-0.7.0.dist-info/METADATA,sha256=QaX8CCdAbVNESyK1jCJU-ykpKXYtXQUcdFQYh9La0R0,494
4
- gcp_platforms_auto-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- gcp_platforms_auto-0.7.0.dist-info/top_level.txt,sha256=4q-ofPMmvBaTnIbAzs-Wp_OwheAVxxmJ1fW9vl3-kyE,19
6
- gcp_platforms_auto-0.7.0.dist-info/RECORD,,