-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #639 from smoy/google-workspace-users
Implement import mechanism for Google Workspace users
- Loading branch information
Showing
6 changed files
with
338 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, Optional | ||
|
||
from pydantic import Field | ||
|
||
from iambic.core.iambic_enum import IambicManaged | ||
from iambic.core.models import BaseModel, BaseTemplate, ExpiryModel | ||
|
||
if TYPE_CHECKING: | ||
from iambic.plugins.v0_1_0.google_workspace.iambic_plugin import GoogleProject | ||
|
||
|
||
GOOGLE_USER_TEMPLATE_TYPE = "NOQ::GoogleWorkspace::User" | ||
|
||
|
||
# https://developers.google.com/admin-sdk/directory/reference/rest/v1/users#UserName | ||
class WorkspaceUserName(BaseModel): | ||
family_name: str = Field( | ||
alias="familyName", | ||
description="The user's last name. Required when creating a user account.", | ||
) | ||
|
||
given_name: str = Field( | ||
alias="givenName", | ||
description="The user's first name. Required when creating a user account.", | ||
) | ||
|
||
display_name: Optional[str] = Field( | ||
alias="displayName", | ||
description="The user's display name. Limit: 256 characters.", | ||
) | ||
|
||
|
||
# https://developers.google.com/admin-sdk/directory/reference/rest/v1/users | ||
class WorkspaceUser(BaseModel, ExpiryModel): | ||
primary_email: str = Field( | ||
description="The user's primary email address. This property is required in a request to create a user account. The primaryEmail must be unique and cannot be an alias of another user.", | ||
) | ||
id: Optional[str] = Field( | ||
None, | ||
description="The unique ID for the user. A user id can be used as a user request URI's userKey.", | ||
) | ||
name: WorkspaceUserName = Field( | ||
description="Holds the given and family names of the user, and the read-only fullName value. The maximum number of characters in the givenName and in the familyName values is 60. In addition, name values support unicode/UTF-8 characters, and can contain spaces, letters (a-z), numbers (0-9), dashes (-), forward slashes (/), and periods (.). For more information about character usage rules, see the administration help center. Maximum allowed data size for this field is 1KB.", | ||
) | ||
|
||
domain: str = Field( | ||
description="this is not direct from user object from google response, but since user maps to a domain, we need to keep track of this information", | ||
) | ||
|
||
@property | ||
def resource_type(self): | ||
return "google:user" | ||
|
||
@property | ||
def resource_id(self): | ||
return self.primary_email | ||
|
||
|
||
class GoogleWorkspaceUserTemplate(BaseTemplate, ExpiryModel): | ||
template_type = GOOGLE_USER_TEMPLATE_TYPE | ||
template_schema_url = ( | ||
"https://docs.iambic.org/reference/schemas/google_workspace_user_template" | ||
) | ||
# owner metadata seems strange for a user (maybe it makes more sense if its machine user) | ||
# owner: Optional[str] = Field(None, description="Owner of the group") | ||
properties: WorkspaceUser | ||
|
||
@property | ||
def resource_type(self): | ||
return "google:user" | ||
|
||
@property | ||
def resource_id(self) -> str: | ||
return self.properties.primary_email | ||
|
||
@property | ||
def default_file_path(self): | ||
file_name = f"{self.properties.primary_email.split('@')[0]}.yaml" | ||
return f"resources/google/users/{self.properties.domain}/{file_name}" | ||
|
||
def _is_iambic_import_only(self, google_project: GoogleProject): | ||
return ( | ||
google_project.iambic_managed == IambicManaged.IMPORT_ONLY | ||
or self.iambic_managed == IambicManaged.IMPORT_ONLY | ||
) | ||
|
||
|
||
# https://googleapis.github.io/google-api-python-client/docs/dyn/admin_directory_v1.users.html#list | ||
async def get_user_template( | ||
service, user: dict, domain: str | ||
) -> GoogleWorkspaceUserTemplate: | ||
# comment out because we don't have to make yet another network call | ||
# members = await get_group_members(service, group) | ||
|
||
file_name = f"{user['primaryEmail'].split('@')[0]}.yaml" | ||
return GoogleWorkspaceUserTemplate( | ||
file_path=f"resources/google/users/{domain}/{file_name}", | ||
properties=dict( | ||
domain=domain, | ||
name=user["name"], | ||
primary_email=user["primaryEmail"], | ||
), | ||
) |
170 changes: 170 additions & 0 deletions
170
iambic/plugins/v0_1_0/google_workspace/user/template_generation.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
import os | ||
from typing import TYPE_CHECKING | ||
|
||
from iambic.core.logger import log | ||
from iambic.core.models import ExecutionMessage | ||
from iambic.core.template_generation import ( | ||
create_or_update_template as common_create_or_update_template, | ||
) | ||
from iambic.core.template_generation import ( | ||
delete_orphaned_templates, | ||
get_existing_template_map, | ||
) | ||
from iambic.plugins.v0_1_0.google_workspace.user.models import ( | ||
GOOGLE_USER_TEMPLATE_TYPE, | ||
GoogleWorkspaceUserTemplate, | ||
) | ||
from iambic.plugins.v0_1_0.google_workspace.user.utils import list_users | ||
|
||
if TYPE_CHECKING: | ||
from iambic.plugins.v0_1_0.google_workspace.iambic_plugin import ( | ||
GoogleProject, | ||
GoogleWorkspaceConfig, | ||
) | ||
|
||
|
||
def get_resource_dir_args(domain: str) -> list: | ||
return ["user", domain] | ||
|
||
|
||
def get_response_dir( | ||
exe_message: ExecutionMessage, google_project: GoogleProject, domain: str | ||
) -> str: | ||
dir_args = get_resource_dir_args(domain) | ||
dir_args.append("templates") | ||
if exe_message.provider_id: | ||
return exe_message.get_directory(*dir_args) | ||
else: | ||
return exe_message.get_directory(google_project.project_id, *dir_args) | ||
|
||
|
||
def get_user_dir( | ||
base_dir: str, | ||
domain: str, | ||
) -> str: | ||
return str( | ||
os.path.join( | ||
base_dir, "resources", "google_workspace", *get_resource_dir_args(domain) | ||
) | ||
) | ||
|
||
|
||
def get_templated_resource_file_path( | ||
resource_dir: str, | ||
resource_email: str, | ||
) -> str: | ||
unwanted_chars = ["}}_", "}}", ".", "-", " "] | ||
resource_name = resource_email.split("@")[0].replace("{{", "").lower() | ||
for unwanted_char in unwanted_chars: | ||
resource_name = resource_name.replace(unwanted_char, "_") | ||
return str(os.path.join(resource_dir, f"{resource_name}.yaml")) | ||
|
||
|
||
async def generate_domain_user_resource_files( | ||
exe_message: ExecutionMessage, project: GoogleProject, domain: str | ||
): | ||
account_user_response_dir = get_response_dir(exe_message, project, domain) | ||
|
||
log.info("Caching Google users.", project_id=project.project_id, domain=domain) | ||
|
||
users = await list_users(domain, project) | ||
for user in users: | ||
user.file_path = os.path.join( | ||
account_user_response_dir, | ||
f"{user.properties.primary_email.split('@')[0]}.yaml", | ||
) | ||
user.write() | ||
|
||
log.info( | ||
"Finished caching Google users.", | ||
project_id=project.project_id, | ||
domain=domain, | ||
user_count=len(users), | ||
) | ||
|
||
|
||
async def update_or_create_user_template( | ||
discovered_user_template: GoogleWorkspaceUserTemplate, | ||
existing_template_map: dict, | ||
user_dir: str, | ||
) -> GoogleWorkspaceUserTemplate: | ||
discovered_user_template.file_path = get_templated_resource_file_path( | ||
user_dir, | ||
discovered_user_template.properties.primary_email, | ||
) | ||
|
||
return common_create_or_update_template( | ||
discovered_user_template.file_path, | ||
existing_template_map, | ||
discovered_user_template.resource_id, | ||
GoogleWorkspaceUserTemplate, | ||
{}, | ||
discovered_user_template.properties, | ||
[], | ||
) | ||
|
||
|
||
async def collect_project_users( | ||
exe_message: ExecutionMessage, config: GoogleWorkspaceConfig | ||
): | ||
assert exe_message.provider_id | ||
project = config.get_workspace(exe_message.provider_id) | ||
log.info("Beginning to retrieve Google users.", project=project.project_id) | ||
|
||
await asyncio.gather( | ||
*[ | ||
generate_domain_user_resource_files(exe_message, project, subject.domain) | ||
for subject in project.subjects | ||
] | ||
) | ||
|
||
log.info("Finished retrieving Google user details", project=project.project_id) | ||
|
||
|
||
async def generate_user_templates( | ||
exe_message: ExecutionMessage, | ||
config: GoogleWorkspaceConfig, | ||
output_dir: str, | ||
detect_messages: list = None, | ||
): | ||
"""List all users in the domain""" | ||
|
||
base_path = os.path.expanduser(output_dir) | ||
existing_template_map = await get_existing_template_map( | ||
base_path, | ||
GOOGLE_USER_TEMPLATE_TYPE, | ||
config.template_map, | ||
) | ||
all_resource_ids = set() | ||
|
||
log.info("Updating and creating Google user templates.") | ||
|
||
for workspace in config.workspaces: | ||
for subject in workspace.subjects: | ||
domain = subject.domain | ||
user_dir = get_user_dir(base_path, domain) | ||
|
||
users = await exe_message.get_sub_exe_files( | ||
*get_resource_dir_args(domain), | ||
"templates", | ||
file_name_and_extension="**.yaml", | ||
) | ||
# Update or create templates | ||
for user in users: | ||
user = GoogleWorkspaceUserTemplate(file_path="unset", **user) | ||
user.file_path = user.default_file_path | ||
resource_template = await update_or_create_user_template( | ||
user, existing_template_map, user_dir | ||
) | ||
if not resource_template: | ||
# Template not updated. Most likely because it's an `enforced` template. | ||
continue | ||
all_resource_ids.add(resource_template.resource_id) | ||
|
||
# Delete templates that no longer exist | ||
delete_orphaned_templates(list(existing_template_map.values()), all_resource_ids) | ||
|
||
log.info("Finish updating and creating Google user templates.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING | ||
|
||
from googleapiclient import _auth | ||
|
||
from iambic.core.logger import log | ||
from iambic.core.utils import aio_wrapper | ||
from iambic.plugins.v0_1_0.google_workspace.user.models import ( | ||
GoogleWorkspaceUserTemplate, | ||
) | ||
|
||
if TYPE_CHECKING: | ||
from iambic.plugins.v0_1_0.google_workspace.iambic_plugin import GoogleProject | ||
|
||
|
||
# https://googleapis.github.io/google-api-python-client/docs/dyn/admin_directory_v1.users.html | ||
async def list_users( | ||
domain: str, google_project: GoogleProject | ||
) -> list[GoogleWorkspaceUserTemplate]: | ||
from iambic.plugins.v0_1_0.google_workspace.user.models import get_user_template | ||
|
||
users = [] | ||
try: | ||
service = await google_project.get_service_connection( | ||
"admin", "directory_v1", domain | ||
) | ||
if not service: | ||
return [] | ||
http = _auth.authorized_http(service._http.credentials) | ||
except AttributeError as err: | ||
log.exception("Unable to process google users.", error=err) | ||
raise | ||
|
||
req = await aio_wrapper(service.users().list, domain=domain) | ||
while req is not None: | ||
res = req.execute(http=http) | ||
if res and "users" in res: | ||
for user in res["users"]: | ||
user_template = await get_user_template(service, user, domain) | ||
users.append(user_template) | ||
|
||
# handle pagination based on https://googleapis.github.io/google-api-python-client/docs/pagination.html | ||
req = await aio_wrapper(service.users().list_next, req, res) | ||
return users |