From a441f0c5de0ccc4eea75e77c0cb26e455961d66b Mon Sep 17 00:00:00 2001 From: Emma Rogge Date: Tue, 6 May 2025 19:24:09 +0000 Subject: [PATCH 1/3] Update FHOW notebooks due to org-linked groups Update First Hour on Workbench notebooks to be compatible with org-linked Workbench groups and to explain about the differences between legacy groups and org-linked groups. --- first_hour_on_vwb/batch_template.csv | 2 +- first_hour_on_vwb/vwb_org_group_utils.py | 38 +++++ first_hour_on_vwb/widget_utils.py | 45 +++-- first_hour_on_vwb/working_with_groups.ipynb | 174 +++++++++++++++----- 4 files changed, 206 insertions(+), 53 deletions(-) create mode 100644 first_hour_on_vwb/vwb_org_group_utils.py diff --git a/first_hour_on_vwb/batch_template.csv b/first_hour_on_vwb/batch_template.csv index 75becdd..2fa6ece 100644 --- a/first_hour_on_vwb/batch_template.csv +++ b/first_hour_on_vwb/batch_template.csv @@ -1,4 +1,4 @@ -WORKBENCH_USER_EMAIL,POLICY +WORKBENCH_USER_EMAIL,ROLE EXAMPLE_USER1@domain.com,MEMBER EXAMPLE_USER2@domain.com,MEMBER EXAMPLE_USER3@domain.com,MEMBER \ No newline at end of file diff --git a/first_hour_on_vwb/vwb_org_group_utils.py b/first_hour_on_vwb/vwb_org_group_utils.py new file mode 100644 index 0000000..84dc9b8 --- /dev/null +++ b/first_hour_on_vwb/vwb_org_group_utils.py @@ -0,0 +1,38 @@ +import json +import subprocess +import sys + +def get_org_linked_group_roles(org_id, group_name): + """ + Return a flattened mapping of users to roles for a named org-linked group and org ID. + """ + roles_dict = {role: set() for role in ["ADMIN", "MEMBER", "READER", "SUPPORT"]} + + wb_command = ["wb","group","role","list",f"--org={org_id}",f"--name={group_name}","--format=JSON"] + result = subprocess.run(wb_command,capture_output=True,text=True) + nested_roles = json.loads(result.stdout) + + for item in nested_roles: + if item['principal']['principalType'] == "GROUP": + roles_dict[role].update(get_org_linked_group_roles(item['principal']['groupOrg'], item['principal']['groupName'])["MEMBER"]) + else: + for role in item['roles']: + roles_dict[role].add(item['principal']['userEmail']) + return roles_dict + +def get_flat_roles_html(roles_dict): + html = f"""""" + html += "" + html += "" + html += "" + for role in roles_dict: + html += "" + if roles_dict[role] != set(): + users = "\n".join(str(e) for e in sorted(roles_dict[role])) + html += f"" + html += f"" + else: + html += f"" + html += f"" + html += "
ROLEUSER_EMAILS
{role}{users}{role}NONE
" + return html \ No newline at end of file diff --git a/first_hour_on_vwb/widget_utils.py b/first_hour_on_vwb/widget_utils.py index 3d90922..5216809 100644 --- a/first_hour_on_vwb/widget_utils.py +++ b/first_hour_on_vwb/widget_utils.py @@ -40,25 +40,24 @@ def list_bq_tables(json_string): for table in tables: all_tables.append(f"{row['projectId']}.{row['datasetId']}.{table.table_id}") return all_tables - -def list_groups(json_string): +def list_legacy_groups(json_string): """ List VWB groups in which user is a member in HTML table form. """ html = f"""""" json_data = json.loads(json_string) + # html += "" html += "" - html += "" - html += "" + html += "" for row in json_data: - html += "" - html += f"" - html += f"" - html += f"" + if not row['orgId']: + html += "" + html += f"" + html += f"" html += "
ORG_IDNAMEEMAILPOLICIESCURRENT_USER_ROLES
{row['name']}{row['email']}{row['currentUserPolicies'][0]}
{row['name']}{sorted(row['currentUserRoles'])}
" return html -def list_group_members(json_string): +def list_legacy_group_members(json_string): """ List members of a VWB group in HTML table form. """ @@ -67,11 +66,31 @@ def list_group_members(json_string): html += "EMAIL" html += "POLICIES" for row in json_data: - html += "" - html += f"{row['email']}" - html += f"{row['policies'][0]}" + if row['orgId']: + html += "" + html += f"{row['email']}" + html += f"{sorted(row['policies'])}" + html += "" + return html + + +def list_org_groups(json_string): + """ List VWB groups in which user is a member in HTML table form. + """ + html = f"""""" + json_data = json.loads(json_string) + html += "" + html += "" + html += "" + for row in json_data: + if row['orgId'] != 'null': + html += "" + html += f"" + html += f"" + html += f"" html += "
ORG_IDNAMECURRENT_USER_ROLES
{row['orgId']}{row['name']}{sorted(row['currentUserRoles'])}
" return html + def list_data_collections(json_string): """ @@ -202,4 +221,4 @@ def __init__(self): def get(self): '''Return widget.''' - return self.checkbox + return self.checkbox \ No newline at end of file diff --git a/first_hour_on_vwb/working_with_groups.ipynb b/first_hour_on_vwb/working_with_groups.ipynb index 5beb224..147c2ef 100644 --- a/first_hour_on_vwb/working_with_groups.ipynb +++ b/first_hour_on_vwb/working_with_groups.ipynb @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": { "tags": [] }, @@ -75,7 +75,9 @@ "from IPython.display import display, HTML\n", "from typing import List\n", "import csv\n", + "import json\n", "import widget_utils as wu\n", + "import vwb_org_group_utils as ogu\n", "import ipywidgets as widgets\n", "import subprocess" ] @@ -86,22 +88,36 @@ "tags": [] }, "source": [ - "## View your Verily Workbench group memberships\n", - "\n", + "## Legacy Verily Workbench groups\n", + "\n", "\n", - "Run the cell below to view a list of the Verily Workbench groups in which you are a member. You should be a member of at least one group, specific to your organization, which will have the name `-users`." + "### View legacy Workbench groups\n", + "On April 14, 2025, Workbench rolled out a change to the implementation of Workbench groups that allows users to create groups which are linked to their organization. Prior to this change, all groups were global in scope -- not linked to any organization. We refer to these global groups which predate this change as \"legacy Workbench groups\". To view any legacy Workbench groups in which you have a role, run the cell below." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
NAMECURRENT_USER_ROLES
3p-sensors-workspace-write['ADMIN']
aou-drc-gcp-pod['MEMBER']
aou-initial-credit-stable-pod['MEMBER']
aou-stable-rt['ADMIN', 'MEMBER']
cdm-adhd-owner['MEMBER']
cdm-demo-1-ws-owners['MEMBER']
cdm-demo-1-ws-writers['MEMBER']
cdm-sjogren-owner['MEMBER']
cdm-workspace-write['ADMIN']
colgate-ohs-vwb-dd-owners['MEMBER']
colgate-ohs-vwb-demo-ws-owners['ADMIN']
colgate-ohs-vwb-demo-ws-readers['ADMIN']
csp-colgate-ohs-prod-prescreen-screen-owners['MEMBER']
csp-colgate-ohs-prod-prescreen-screen-readers['MEMBER']
csp-colgate-ohs-readers['MEMBER']
dd-prod-vwb['ADMIN']
demo-1-ws-owners['ADMIN', 'MEMBER']
demo-1-ws-writers['MEMBER']
demo-gcp-pod['MEMBER']
developer-admins['ADMIN']
emmarogge['ADMIN', 'MEMBER']
fraud-model-workspace-write['ADMIN']
groups-demo['ADMIN', 'MEMBER']
gsk-ip-users['ADMIN']
gsk-users['ADMIN']
hhr-internal-users['ADMIN']
huji-users['ADMIN']
jiksak-users['ADMIN']
kyverna-users['ADMIN']
mayo-clinic-users['ADMIN']
michaeljfox-users['ADMIN']
ml-infra-gcp-pod['MEMBER']
ml-ops-breakglass['MEMBER']
nashbio-users['ADMIN']
nashvillebiosciences-users['ADMIN']
onduo-zus-workspace-write['ADMIN']
otsuka-users['ADMIN']
oxford-imcm-gcp-pod['MEMBER']
php-integrations['ADMIN']
polytechnique-users['ADMIN']
post-ov-verifiers['ADMIN']
roche-users['ADMIN']
rometx-ip-users['ADMIN']
rrwd_ds_mimic['ADMIN']
sean-test-users['ADMIN']
tecnico-lisboa-users['ADMIN']
terra-users-devel-shadow['ADMIN']
ucsd-users['ADMIN']
umich-users['ADMIN']
unruh-users['ADMIN']
utsw-workspace-write['ADMIN']
verily-aod-owner['ADMIN', 'MEMBER']
verily-org['MEMBER']
verily-published-data-pod-admins['ADMIN']
verily-published-data-pod-users['ADMIN']
verily-sandbox-pod['MEMBER']
verily-users['ADMIN', 'MEMBER']
verilystudies-test-gcp-pod['MEMBER']
vphp-dd-eng['ADMIN']
vphp-ml-ops['ADMIN']
vumc-early-adopters['ADMIN', 'MEMBER']
vwb-eng-gcp-pod['MEMBER']
vwb-public-data-owners['ADMIN', 'MEMBER']
vwb-public-data-readers['ADMIN', 'MEMBER']
vwb-solutions-team['ADMIN', 'MEMBER']
vwb_support_candidates['MEMBER']
vwb_support_candidates_preprod['ADMIN']
wastewater-test-bucket-dc-reader['ADMIN', 'MEMBER']
workbench-audit-trail-M1-readers['ADMIN', 'MEMBER']
workbench-superadmins-prod['ADMIN']
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "result = subprocess.run(['wb','group','list','--format=JSON'],capture_output=True,text=True)\n", - "print(result.stderr) if not result.stdout else display(HTML(wu.list_groups(result.stdout)))" + "print(result.stderr) if not result.stdout else display(HTML(wu.list_legacy_groups(result.stdout)))" ] }, { @@ -110,18 +126,34 @@ "tags": [] }, "source": [ - "## View members of a Verily Workbench group\n", + "### View members of legacy Verily Workbench groups\n", "\n", - "In order to view the members of a Verily Workbench group, you must be an owner of that group. To investigate the membership of your group, run the cell below to create a widget. Provide the group name in the widget input field and click the button to view a list of the members of the desired group." + "In order to view the members of a legacy Verily Workbench group, you must be an admin of that group. To investigate the membership of your group, run the cell below to create a widget. Provide the group name in the widget input field and click the button to view a list of the members of the desired group." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "metadata": { + "scrolled": true, "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5a93efcd1fb242a09fa895b5487efdfe", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Label(value='Please provide the name for a Verily Workbench group.'), Text(value='', descriptio…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "class ListMembersWidget(object):\n", " def __init__(self):\n", @@ -132,21 +164,84 @@ " self.vb = widgets.VBox([self.label,self.input_group_name,self.list_button,self.output],layout=wu.vbox_layout)\n", " self.list_button.on_click(self.list_members)\n", "\n", - " \n", + "\n", " def list_members(self,b):\n", + " self.output.clear_output()\n", " with self.output:\n", " wb_command = [\"wb\",\"group\",\"list-users\",f\"--name={self.input_group_name.value}\",\"--format=JSON\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " if not result.stdout:\n", " self.output.append_display_data(result.stderr)\n", " else:\n", - " self.output.append_display_data(HTML(wu.list_group_members(result.stdout)))\n", + " self.output.append_display_data(HTML(wu.list_legacy_group_members(result.stdout)))\n", "\n", "# Create widget \n", "list_members_widget = ListMembersWidget()\n", "display(list_members_widget.vb)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Org-linked Verily Workbench groups\n", + "\n", + "\n", + "### View org-linked Workbench groups\n", + "On April 14, 2025, Workbench rolled out a change to the implementation of Workbench groups that allows users to create groups which are linked to their organization. Prior to this change, all groups were global in scope -- not linked to any organization. We refer to these groups as \"org-linked\" groups. To view any org-linked Workbench groups in which you have a role, run the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result = subprocess.run([\"wb\",\"group\",\"list\",\"--format=JSON\"],capture_output=True,text=True)\n", + "display(HTML(wu.list_org_groups(result.stdout)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### View members of org-linked Verily Workbench groups\n", + "\n", + "In order to view the members of a org-linked Verily Workbench group, you must either be an admin of that group or an org admin. To investigate the membership of your group, run the cell below to create a widget. Provide the *group name* and the *org ID* in the widget input fields and click the button to view a list of the members of the desired group." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class ListOrgLinkedMembersWidget(object):\n", + " def __init__(self):\n", + " self.label = widgets.Label(value='Please provide the name for a Verily Workbench group.')\n", + " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", + " self.input_org_id = wu.TextInputWidget('',\"Org ID:\").get()\n", + " self.output = widgets.Output()\n", + " self.list_button = wu.StyledButton('List users in group','Click to list users in this Verily Workbench group.','list').get()\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.list_button,self.output],layout=wu.vbox_layout)\n", + " self.list_button.on_click(self.list_members)\n", + " \n", + " def list_members(self,b):\n", + " self.output.clear_output()\n", + " with self.output:\n", + " group_roles = ogu.get_org_linked_group_roles(self.input_org_id.value, self.input_group_name.value)\n", + " html_roles = ogu.get_flat_roles_html(group_roles)\n", + " self.output.append_display_data(HTML(html_roles))\n", + "\n", + "# Create widget \n", + "list_org_linked_members_widget = ListOrgLinkedMembersWidget()\n", + "display(list_org_linked_members_widget.vb)" + ] + }, { "cell_type": "markdown", "metadata": { @@ -180,17 +275,18 @@ "source": [ "class CreateGroupWidget(object):\n", " def __init__(self):\n", - " self.label = widgets.Label(value='Please provide the name for a Verily Workbench group.')\n", + " self.label = widgets.Label(value='Please provide the name & org ID for your Verily Workbench group.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", + " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Create group','Click to create a Verily Workbench group.','plus').get()\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.button,self.output],layout=wu.vbox_layout)\n", " self.button.on_click(self.create_group)\n", "\n", " def create_group(self,b):\n", " with self.output:\n", " self.output.clear_output()\n", - " wb_command = [\"wb\",\"group\",\"create\",f\"--name={self.input_group_name.value}\",\"--format=text\"]\n", + " wb_command = [\"wb\",\"group\",\"create\",f\"--name={self.input_group_name.value}\",f\"--org={self.input_org_id.value}\",\"--format=text\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -235,15 +331,16 @@ " def __init__(self):\n", " self.label = widgets.Label(value='Please provide the name for a Verily Workbench group.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", + " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Delete group','Click to delete a Verily Workbench group.','trash').get()\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.button,self.output],layout=wu.vbox_layout)\n", " self.button.on_click(self.delete_group)\n", "\n", " def delete_group(self,b):\n", " with self.output:\n", " self.output.clear_output()\n", - " wb_command = [\"wb\",\"group\",\"delete\",f\"--name={self.input_group_name.value}\",\"--quiet\"]\n", + " wb_command = [\"wb\",\"group\",\"delete\",f\"--name={self.input_group_name.value}\",f\"--org={self.input_org_id.value}\",\"--quiet\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -273,10 +370,10 @@ "tags": [] }, "source": [ - "### Add a user to a Verily Workbench group\n", + "### Add a user to a Workbench group\n", "\n", "1. Run the cell below to create a widget that adds a user to a Verily Workbench group.\n", - "1. In the widget, provide the group name for an existing Verily Workbench group and the email of the user you wish to add. \n", + "1. In the widget, provide the group name and org ID of an existing Verily Workbench group and the email of the user you wish to add. \n", "1. Click the button to add the user to the group!\n", "\n", "You should then see output resembling:\n", @@ -299,12 +396,14 @@ " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", " self.input_user_email = wu.TextInputWidget('',\"User Email:\").get()\n", + " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.policy_drop_down = wu.DropdownInputWidget(['MEMBER','ADMIN'],'MEMBER',\"Policy:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Add user','Click to add a user to the group.','user-plus').get()\n", " self.vb = widgets.VBox(\n", " [self.label,\n", " self.input_group_name,\n", + " self.input_org_id,\n", " self.input_user_email,\n", " self.policy_drop_down,\n", " self.button,\n", @@ -314,7 +413,7 @@ "\n", " def add_user(self,b):\n", " with self.output:\n", - " wb_command = [\"wb\", \"group\", \"add-user\",f\"--name={self.input_group_name.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", + " wb_command = [\"wb\", \"group\", \"add-user\",f\"--name={self.input_group_name.value}\",f\"--name={self.input_org_id.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -353,6 +452,7 @@ " def __init__(self):\n", " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", + " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.input_user_email = wu.TextInputWidget('',\"User Email:\").get()\n", " self.policy_drop_down = wu.DropdownInputWidget(['MEMBER','ADMIN'],'MEMBER',\"Policy:\").get()\n", " self.output = widgets.Output()\n", @@ -360,6 +460,7 @@ " self.vb = widgets.VBox(\n", " [self.label,\n", " self.input_group_name,\n", + " self.input_org_id,\n", " self.input_user_email,\n", " self.policy_drop_down,\n", " self.button,\n", @@ -369,7 +470,7 @@ "\n", " def remove_user(self,b):\n", " with self.output:\n", - " wb_command = [\"wb\", \"group\", \"remove-user\",f\"--name={self.input_group_name.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", + " wb_command = [\"wb\", \"group\", \"remove-user\",f\"--name={self.input_group_name.value}\",f\"--name={self.input_org_id.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -388,14 +489,14 @@ "\n", "Run the cell below to create a widget. The widget takes the following inputs:\n", "- `group name`: The name of an existing Verily Workbench group of which you are an admin.\n", - "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"POLICY\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", + "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"ROLE\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", "\n", "Once you've created a CSV with your users' information and populated the input fields, click the button to add a batch of users from the Verily Workbench group.\n", "\n", "For each user, you should see output resembling:\n", "```\n", "User added to Workbench group.\n", - ": \n", + ": \n", "```" ] }, @@ -411,11 +512,12 @@ " def __init__(self):\n", " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\",).get()\n", + " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.input_file = wu.TextInputWidget('',\"CSV File:\",).get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Add users','Click to add a batch of users to the group.','user-plus').get()\n", " self.button.on_click(self.batch_add_users)\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", "\n", " def batch_add_users(self,b):\n", " with self.output:\n", @@ -423,7 +525,7 @@ " with open(self.input_file.value) as csv_file:\n", " csv_reader = csv.DictReader(csv_file,delimiter=',')\n", " for user in csv_reader:\n", - " wb_command = [\"wb\", \"group\", \"add-user\",f\"--name={self.input_group_name.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--policy={user['POLICY']}\"]\n", + " wb_command = [\"wb\", \"group\", \"role\",\"grant\", \"user\",f\"--name={self.input_group_name.value}\",f\"--name={self.input_org_id.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--role={user['ROLE']}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -444,7 +546,7 @@ "\n", "Run the cell below to create a widget. The widget takes the following inputs:\n", "- `group name`: The name of an existing Verily Workbench group of which you are an admin.\n", - "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"POLICY\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", + "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"ROLE\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", "\n", "Once you've created a CSV with your users' information and populated the input fields, click the button to remove a batch of users from the Verily Workbench group.\n", "\n", @@ -466,11 +568,12 @@ " def __init__(self):\n", " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", + " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.input_file = wu.TextInputWidget('',\"CSV File:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Remove users','Click to remove a batch of users to the group.','user-minus').get()\n", " self.button.on_click(self.batch_remove_users)\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", "\n", " def batch_remove_users(self,b):\n", " with self.output:\n", @@ -478,7 +581,7 @@ " with open(self.input_file.value) as csv_file:\n", " csv_reader = csv.DictReader(csv_file,delimiter=',')\n", " for user in csv_reader:\n", - " wb_command = [\"wb\", \"group\", \"remove-user\",f\"--name={self.input_group_name.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--policy={user['POLICY']}\"]\n", + " wb_command = [\"wb\", \"group\", \"role\", \"revoke\", \"user\", f\"--name={self.input_group_name.value}\",f\"--name={self.input_group_name.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--policy={user['ROLE']}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -584,19 +687,12 @@ "metadata": {}, "source": [ "---\n", - "Copyright 2024 Verily Life Sciences LLC\n", + "Copyright 2025 Verily Life Sciences LLC\n", "\n", "Use of this source code is governed by a BSD-style \n", "license that can be found in the LICENSE file or at \n", "https://developers.google.com/open-source/licenses/bsd" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -607,7 +703,7 @@ "uri": "gcr.io/deeplearning-platform-release/:m115" }, "kernelspec": { - "display_name": "Python 3 (Local)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -621,7 +717,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.16" } }, "nbformat": 4, From 60958714f65387032d073a99c6b5ccc8d77eb126 Mon Sep 17 00:00:00 2001 From: Emma Rogge Date: Mon, 12 May 2025 18:00:09 +0000 Subject: [PATCH 2/3] Update Working with Groups notebook Update Working with Groups notebook to address org-linked groups, in addition to legacy groups. Add utility method for flat-listing members of nested groups. --- first_hour_on_vwb/vwb_org_group_utils.py | 7 +- first_hour_on_vwb/working_with_groups.ipynb | 143 ++++++++++---------- 2 files changed, 73 insertions(+), 77 deletions(-) diff --git a/first_hour_on_vwb/vwb_org_group_utils.py b/first_hour_on_vwb/vwb_org_group_utils.py index 84dc9b8..ab92ca1 100644 --- a/first_hour_on_vwb/vwb_org_group_utils.py +++ b/first_hour_on_vwb/vwb_org_group_utils.py @@ -17,7 +17,9 @@ def get_org_linked_group_roles(org_id, group_name): roles_dict[role].update(get_org_linked_group_roles(item['principal']['groupOrg'], item['principal']['groupName'])["MEMBER"]) else: for role in item['roles']: - roles_dict[role].add(item['principal']['userEmail']) + if item['principal']['userEmail'] != None: + roles_dict[role].add(item['principal']['userEmail']) + return roles_dict def get_flat_roles_html(roles_dict): @@ -28,7 +30,8 @@ def get_flat_roles_html(roles_dict): for role in roles_dict: html += "" if roles_dict[role] != set(): - users = "\n".join(str(e) for e in sorted(roles_dict[role])) + print(f"role: {role}, users: {roles_dict[role]}") + users = ", ".join(str(e) for e in sorted(roles_dict[role])) html += f"{role}" html += f"{users}" else: diff --git a/first_hour_on_vwb/working_with_groups.ipynb b/first_hour_on_vwb/working_with_groups.ipynb index 147c2ef..f116a76 100644 --- a/first_hour_on_vwb/working_with_groups.ipynb +++ b/first_hour_on_vwb/working_with_groups.ipynb @@ -32,7 +32,8 @@ { "cell_type": "markdown", "metadata": { - "id": "d975e698c9a4" + "id": "d975e698c9a4", + "jp-MarkdownHeadingCollapsed": true }, "source": [ "### Objective\n", @@ -66,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": { "tags": [] }, @@ -75,9 +76,8 @@ "from IPython.display import display, HTML\n", "from typing import List\n", "import csv\n", - "import json\n", "import widget_utils as wu\n", - "import vwb_org_group_utils as ogu\n", + "import vwb_org_group_utils as vogu\n", "import ipywidgets as widgets\n", "import subprocess" ] @@ -88,33 +88,20 @@ "tags": [] }, "source": [ - "## Legacy Verily Workbench groups\n", - "\n", + "## Legacy Verily Workbench group memberships\n", + "\n", "\n", - "### View legacy Workbench groups\n", "On April 14, 2025, Workbench rolled out a change to the implementation of Workbench groups that allows users to create groups which are linked to their organization. Prior to this change, all groups were global in scope -- not linked to any organization. We refer to these global groups which predate this change as \"legacy Workbench groups\". To view any legacy Workbench groups in which you have a role, run the cell below." ] }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "metadata": { + "scrolled": true, "tags": [] }, - "outputs": [ - { - "data": { - "text/html": [ - "
NAMECURRENT_USER_ROLES
3p-sensors-workspace-write['ADMIN']
aou-drc-gcp-pod['MEMBER']
aou-initial-credit-stable-pod['MEMBER']
aou-stable-rt['ADMIN', 'MEMBER']
cdm-adhd-owner['MEMBER']
cdm-demo-1-ws-owners['MEMBER']
cdm-demo-1-ws-writers['MEMBER']
cdm-sjogren-owner['MEMBER']
cdm-workspace-write['ADMIN']
colgate-ohs-vwb-dd-owners['MEMBER']
colgate-ohs-vwb-demo-ws-owners['ADMIN']
colgate-ohs-vwb-demo-ws-readers['ADMIN']
csp-colgate-ohs-prod-prescreen-screen-owners['MEMBER']
csp-colgate-ohs-prod-prescreen-screen-readers['MEMBER']
csp-colgate-ohs-readers['MEMBER']
dd-prod-vwb['ADMIN']
demo-1-ws-owners['ADMIN', 'MEMBER']
demo-1-ws-writers['MEMBER']
demo-gcp-pod['MEMBER']
developer-admins['ADMIN']
emmarogge['ADMIN', 'MEMBER']
fraud-model-workspace-write['ADMIN']
groups-demo['ADMIN', 'MEMBER']
gsk-ip-users['ADMIN']
gsk-users['ADMIN']
hhr-internal-users['ADMIN']
huji-users['ADMIN']
jiksak-users['ADMIN']
kyverna-users['ADMIN']
mayo-clinic-users['ADMIN']
michaeljfox-users['ADMIN']
ml-infra-gcp-pod['MEMBER']
ml-ops-breakglass['MEMBER']
nashbio-users['ADMIN']
nashvillebiosciences-users['ADMIN']
onduo-zus-workspace-write['ADMIN']
otsuka-users['ADMIN']
oxford-imcm-gcp-pod['MEMBER']
php-integrations['ADMIN']
polytechnique-users['ADMIN']
post-ov-verifiers['ADMIN']
roche-users['ADMIN']
rometx-ip-users['ADMIN']
rrwd_ds_mimic['ADMIN']
sean-test-users['ADMIN']
tecnico-lisboa-users['ADMIN']
terra-users-devel-shadow['ADMIN']
ucsd-users['ADMIN']
umich-users['ADMIN']
unruh-users['ADMIN']
utsw-workspace-write['ADMIN']
verily-aod-owner['ADMIN', 'MEMBER']
verily-org['MEMBER']
verily-published-data-pod-admins['ADMIN']
verily-published-data-pod-users['ADMIN']
verily-sandbox-pod['MEMBER']
verily-users['ADMIN', 'MEMBER']
verilystudies-test-gcp-pod['MEMBER']
vphp-dd-eng['ADMIN']
vphp-ml-ops['ADMIN']
vumc-early-adopters['ADMIN', 'MEMBER']
vwb-eng-gcp-pod['MEMBER']
vwb-public-data-owners['ADMIN', 'MEMBER']
vwb-public-data-readers['ADMIN', 'MEMBER']
vwb-solutions-team['ADMIN', 'MEMBER']
vwb_support_candidates['MEMBER']
vwb_support_candidates_preprod['ADMIN']
wastewater-test-bucket-dc-reader['ADMIN', 'MEMBER']
workbench-audit-trail-M1-readers['ADMIN', 'MEMBER']
workbench-superadmins-prod['ADMIN']
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "result = subprocess.run(['wb','group','list','--format=JSON'],capture_output=True,text=True)\n", "print(result.stderr) if not result.stdout else display(HTML(wu.list_legacy_groups(result.stdout)))" @@ -133,27 +120,12 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": { "scrolled": true, "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5a93efcd1fb242a09fa895b5487efdfe", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(Label(value='Please provide the name for a Verily Workbench group.'), Text(value='', descriptio…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "class ListMembersWidget(object):\n", " def __init__(self):\n", @@ -164,7 +136,7 @@ " self.vb = widgets.VBox([self.label,self.input_group_name,self.list_button,self.output],layout=wu.vbox_layout)\n", " self.list_button.on_click(self.list_members)\n", "\n", - "\n", + " \n", " def list_members(self,b):\n", " self.output.clear_output()\n", " with self.output:\n", @@ -184,21 +156,42 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Org-linked Verily Workbench groups\n", - "\n", + "## Organization-linked Workbench groups\n", "\n", - "### View org-linked Workbench groups\n", - "On April 14, 2025, Workbench rolled out a change to the implementation of Workbench groups that allows users to create groups which are linked to their organization. Prior to this change, all groups were global in scope -- not linked to any organization. We refer to these groups as \"org-linked\" groups. To view any org-linked Workbench groups in which you have a role, run the cell below." + "Starting on April 14, 2025, it is no longer possible to create \"legacy\" Workbench groups. Users must provide an organization when creating groups, and org admins and members can create org-linked groups. To view the org-linked Workbench groups in which you have a role, run the cell below." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "result = subprocess.run([\"wb\",\"group\",\"list\",\"--format=JSON\"],capture_output=True,text=True)\n", - "display(HTML(wu.list_org_groups(result.stdout)))" + "class ListOrgGroupsWidget(object):\n", + " def __init__(self):\n", + " self.label = widgets.Label(value='Please provide the org ID for a Verily Workbench org.')\n", + " self.input_org_id = wu.TextInputWidget('',\"Org ID:\").get()\n", + " self.output = widgets.Output()\n", + " self.list_button = wu.StyledButton('List org-linked group','Click to list org-linked groups.','list').get()\n", + " self.vb = widgets.VBox([self.label,self.input_org_id,self.list_button,self.output],layout=wu.vbox_layout)\n", + " self.list_button.on_click(self.list_groups)\n", + "\n", + " \n", + " def list_groups(self,b):\n", + " self.output.clear_output()\n", + " with self.output:\n", + " wb_command = ['wb', 'org','group','list',f\"--org={self.input_org_id.value}\",\"--format=JSON\"]\n", + " result = subprocess.run(wb_command,capture_output=True,text=True)\n", + " if not result.stdout:\n", + " self.output.append_display_data(result.stderr)\n", + " else:\n", + " self.output.append_display_data(HTML(wu.list_org_groups(result.stdout)))\n", + "\n", + "# Create widget \n", + "list_org_groups_widget = ListOrgGroupsWidget()\n", + "display(list_org_groups_widget.vb)" ] }, { @@ -207,7 +200,7 @@ "tags": [] }, "source": [ - "### View members of org-linked Verily Workbench groups\n", + "## View members of org-linked Verily Workbench groups\n", "\n", "In order to view the members of a org-linked Verily Workbench group, you must either be an admin of that group or an org admin. To investigate the membership of your group, run the cell below to create a widget. Provide the *group name* and the *org ID* in the widget input fields and click the button to view a list of the members of the desired group." ] @@ -216,11 +209,12 @@ "cell_type": "code", "execution_count": null, "metadata": { + "scrolled": true, "tags": [] }, "outputs": [], "source": [ - "class ListOrgLinkedMembersWidget(object):\n", + "class ListMembersWidget(object):\n", " def __init__(self):\n", " self.label = widgets.Label(value='Please provide the name for a Verily Workbench group.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", @@ -229,17 +223,16 @@ " self.list_button = wu.StyledButton('List users in group','Click to list users in this Verily Workbench group.','list').get()\n", " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.list_button,self.output],layout=wu.vbox_layout)\n", " self.list_button.on_click(self.list_members)\n", - " \n", + "\n", + " \n", " def list_members(self,b):\n", " self.output.clear_output()\n", " with self.output:\n", - " group_roles = ogu.get_org_linked_group_roles(self.input_org_id.value, self.input_group_name.value)\n", - " html_roles = ogu.get_flat_roles_html(group_roles)\n", - " self.output.append_display_data(HTML(html_roles))\n", + " self.output.append_display_data(HTML(vogu.get_flat_roles_html(vogu.get_org_linked_group_roles(self.input_org_id.value, self.input_group_name.value))))\n", "\n", "# Create widget \n", - "list_org_linked_members_widget = ListOrgLinkedMembersWidget()\n", - "display(list_org_linked_members_widget.vb)" + "list_members_widget = ListMembersWidget()\n", + "display(list_members_widget.vb)" ] }, { @@ -331,16 +324,15 @@ " def __init__(self):\n", " self.label = widgets.Label(value='Please provide the name for a Verily Workbench group.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", - " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Delete group','Click to delete a Verily Workbench group.','trash').get()\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.button,self.output],layout=wu.vbox_layout)\n", " self.button.on_click(self.delete_group)\n", "\n", " def delete_group(self,b):\n", " with self.output:\n", " self.output.clear_output()\n", - " wb_command = [\"wb\",\"group\",\"delete\",f\"--name={self.input_group_name.value}\",f\"--org={self.input_org_id.value}\",\"--quiet\"]\n", + " wb_command = [\"wb\",\"group\",\"delete\",f\"--name={self.input_group_name.value}\",\"--quiet\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -370,10 +362,10 @@ "tags": [] }, "source": [ - "### Add a user to a Workbench group\n", + "### Add a user to a Verily Workbench group\n", "\n", "1. Run the cell below to create a widget that adds a user to a Verily Workbench group.\n", - "1. In the widget, provide the group name and org ID of an existing Verily Workbench group and the email of the user you wish to add. \n", + "1. In the widget, provide the group name for an existing Verily Workbench group and the email of the user you wish to add. \n", "1. Click the button to add the user to the group!\n", "\n", "You should then see output resembling:\n", @@ -396,14 +388,12 @@ " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", " self.input_user_email = wu.TextInputWidget('',\"User Email:\").get()\n", - " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.policy_drop_down = wu.DropdownInputWidget(['MEMBER','ADMIN'],'MEMBER',\"Policy:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Add user','Click to add a user to the group.','user-plus').get()\n", " self.vb = widgets.VBox(\n", " [self.label,\n", " self.input_group_name,\n", - " self.input_org_id,\n", " self.input_user_email,\n", " self.policy_drop_down,\n", " self.button,\n", @@ -413,7 +403,7 @@ "\n", " def add_user(self,b):\n", " with self.output:\n", - " wb_command = [\"wb\", \"group\", \"add-user\",f\"--name={self.input_group_name.value}\",f\"--name={self.input_org_id.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", + " wb_command = [\"wb\", \"group\", \"add-user\",f\"--name={self.input_group_name.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -452,7 +442,6 @@ " def __init__(self):\n", " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", - " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.input_user_email = wu.TextInputWidget('',\"User Email:\").get()\n", " self.policy_drop_down = wu.DropdownInputWidget(['MEMBER','ADMIN'],'MEMBER',\"Policy:\").get()\n", " self.output = widgets.Output()\n", @@ -460,7 +449,6 @@ " self.vb = widgets.VBox(\n", " [self.label,\n", " self.input_group_name,\n", - " self.input_org_id,\n", " self.input_user_email,\n", " self.policy_drop_down,\n", " self.button,\n", @@ -470,7 +458,7 @@ "\n", " def remove_user(self,b):\n", " with self.output:\n", - " wb_command = [\"wb\", \"group\", \"remove-user\",f\"--name={self.input_group_name.value}\",f\"--name={self.input_org_id.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", + " wb_command = [\"wb\", \"group\", \"remove-user\",f\"--name={self.input_group_name.value}\",f\"--email={self.input_user_email.value}\",f\"--policy={self.policy_drop_down.value}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -489,14 +477,14 @@ "\n", "Run the cell below to create a widget. The widget takes the following inputs:\n", "- `group name`: The name of an existing Verily Workbench group of which you are an admin.\n", - "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"ROLE\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", + "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"POLICY\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", "\n", "Once you've created a CSV with your users' information and populated the input fields, click the button to add a batch of users from the Verily Workbench group.\n", "\n", "For each user, you should see output resembling:\n", "```\n", "User added to Workbench group.\n", - ": \n", + ": \n", "```" ] }, @@ -512,12 +500,11 @@ " def __init__(self):\n", " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\",).get()\n", - " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.input_file = wu.TextInputWidget('',\"CSV File:\",).get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Add users','Click to add a batch of users to the group.','user-plus').get()\n", " self.button.on_click(self.batch_add_users)\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", "\n", " def batch_add_users(self,b):\n", " with self.output:\n", @@ -525,7 +512,7 @@ " with open(self.input_file.value) as csv_file:\n", " csv_reader = csv.DictReader(csv_file,delimiter=',')\n", " for user in csv_reader:\n", - " wb_command = [\"wb\", \"group\", \"role\",\"grant\", \"user\",f\"--name={self.input_group_name.value}\",f\"--name={self.input_org_id.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--role={user['ROLE']}\"]\n", + " wb_command = [\"wb\", \"group\", \"add-user\",f\"--name={self.input_group_name.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--policy={user['POLICY']}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -546,7 +533,7 @@ "\n", "Run the cell below to create a widget. The widget takes the following inputs:\n", "- `group name`: The name of an existing Verily Workbench group of which you are an admin.\n", - "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"ROLE\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", + "- `csv`: a CSV file located in your cloud environment in the same directory as this notebook. A template CSV file is provided in this directory, [batch_template.csv]('batch_template.csv'), consisting of the headers \"WORKBENCH_USER_EMAIL\" and \"POLICY\", followed by one line per user. Each line contains a user's Verily Workbench email and the corresponding membership policy (either \"MEMBER\" or \"ADMIN\").\n", "\n", "Once you've created a CSV with your users' information and populated the input fields, click the button to remove a batch of users from the Verily Workbench group.\n", "\n", @@ -568,12 +555,11 @@ " def __init__(self):\n", " self.label = widgets.Label(value = 'Please provide appropriate values for the fields below.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", - " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", " self.input_file = wu.TextInputWidget('',\"CSV File:\").get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Remove users','Click to remove a batch of users to the group.','user-minus').get()\n", " self.button.on_click(self.batch_remove_users)\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", + " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_file,self.button,self.output],layout=wu.vbox_layout)\n", "\n", " def batch_remove_users(self,b):\n", " with self.output:\n", @@ -581,7 +567,7 @@ " with open(self.input_file.value) as csv_file:\n", " csv_reader = csv.DictReader(csv_file,delimiter=',')\n", " for user in csv_reader:\n", - " wb_command = [\"wb\", \"group\", \"role\", \"revoke\", \"user\", f\"--name={self.input_group_name.value}\",f\"--name={self.input_group_name.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--policy={user['ROLE']}\"]\n", + " wb_command = [\"wb\", \"group\", \"remove-user\",f\"--name={self.input_group_name.value}\",f\"--email={user['WORKBENCH_USER_EMAIL']}\",f\"--policy={user['POLICY']}\"]\n", " result = subprocess.run(wb_command,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", @@ -693,6 +679,13 @@ "license that can be found in the LICENSE file or at \n", "https://developers.google.com/open-source/licenses/bsd" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From b6aa3d90c2b4900169cdb7b4952323471df57f04 Mon Sep 17 00:00:00 2001 From: Emma Rogge Date: Mon, 12 May 2025 19:19:29 +0000 Subject: [PATCH 3/3] Update group creation widget --- first_hour_on_vwb/widget_utils.py | 56 ++++++++++++++++++--- first_hour_on_vwb/working_with_groups.ipynb | 49 ++++++++++++++---- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/first_hour_on_vwb/widget_utils.py b/first_hour_on_vwb/widget_utils.py index 5216809..3787834 100644 --- a/first_hour_on_vwb/widget_utils.py +++ b/first_hour_on_vwb/widget_utils.py @@ -66,10 +66,9 @@ def list_legacy_group_members(json_string): html += "EMAIL" html += "POLICIES" for row in json_data: - if row['orgId']: - html += "" - html += f"{row['email']}" - html += f"{sorted(row['policies'])}" + html += "" + html += f"{row['email']}" + html += f"{sorted(row['policies'])}" html += "" return html @@ -81,13 +80,13 @@ def list_org_groups(json_string): json_data = json.loads(json_string) html += "ORG_ID" html += "NAME" - html += "CURRENT_USER_ROLES" + # html += "CURRENT_USER_ROLES" for row in json_data: if row['orgId'] != 'null': html += "" html += f"{row['orgId']}" html += f"{row['name']}" - html += f"{sorted(row['currentUserRoles'])}" + # html += f"{sorted(row['currentUserRoles'])}" html += "" return html @@ -168,6 +167,32 @@ def get(self): return self.dropdown_widget +@dataclass +class BoundIntInputWidget: + """A styled input accepting bounded integers widget with layout. + """ + default_value: int = 0 + min_value: int = 0 + max_value: int = 0 + description: str = "" + bound_int_widget: widgets.BoundedIntText = None + + def __post_init__(self): + self.bound_int_widget = widgets.BoundedIntText( + value=self.default_value, + min=self.min_value, + max=self.max_value, + step=1, + description=self.description, + disabled=False, + layout = {'width' : 'max-content'}, + style=input_style + ) + + def get(self): + '''Return widget.''' + return self.bound_int_widget + @dataclass class StyledButton(): """ A styled button widget with layout. @@ -181,7 +206,7 @@ def __post_init__(self): self.button = widgets.Button( description=self.description, disabled=False, - layout=widgets.Layout(width='75%'), + layout=widgets.Layout(width='100%'), display='flex', align_items='stretch', button_style='', @@ -219,6 +244,23 @@ def __init__(self): style={'background': '#D8D2EB'} ) + def get(self): + '''Return widget.''' + return self.checkbox + + +class LongLabelCheckbox: + """ Creates a styled checkbox. + """ + def __init__(self, description): + self.checkbox = widgets.Checkbox( + False, + description = description, + layout = {'width' : 'max-content'}, + disabled = False, + indent = False + ) + def get(self): '''Return widget.''' return self.checkbox \ No newline at end of file diff --git a/first_hour_on_vwb/working_with_groups.ipynb b/first_hour_on_vwb/working_with_groups.ipynb index f116a76..a5b35f0 100644 --- a/first_hour_on_vwb/working_with_groups.ipynb +++ b/first_hour_on_vwb/working_with_groups.ipynb @@ -85,6 +85,7 @@ { "cell_type": "markdown", "metadata": { + "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ @@ -154,7 +155,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "## Organization-linked Workbench groups\n", "\n", @@ -197,6 +200,7 @@ { "cell_type": "markdown", "metadata": { + "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ @@ -244,17 +248,28 @@ "## Create a new Verily Workbench group\n", "\n", "\n", - "Run the cell below to create a widget, then populate the widget's fields and click the button to create a new Verily Workbench group. \n", - "The value provided for the `GROUP_NAME` should be unique, and reflect the purpose of the group (for example, for a group of Verily researchers working on the 1000 Genomes dataset, `verily-1000-genomes-researchers` would be appropriate).\n", + "Run the cell below to create a widget that creates an org-linked Workbench group. Then, populate the widget's input fields:\n", "\n", + "- `Group Name`: Should be unique and reflect the purpose of the group (for example, for a group of Verily researchers working on the 1000 Genomes dataset, `verily-1000-genomes-researchers` would be appropriate).\n", + "- `Org ID`: Your organization's ID.\n", + "- `Expiration`: The default and maximimum MEMBER role expiration in *days*. Zero means the MEMBER role does not expire.\n", + "- `Expiration Notification`: Notify group admins about member expirations.\n", + "- `Require Grant Reason`: Require a reason on grants to the group.\n", + "\n", + "Click the button to create a new Verily Workbench group. \n", "You should see output resembling:\n", "\n", "```\n", "Workbench group created.\n", "\n", - " Email: @verily.com\n", - " # Members: 1\n", - " Current user's policies: ADMIN\n", + " Organization : \n", + " Description : \n", + " Expiration days : \n", + " Expiration notify : \n", + " Sync to Google Group : \n", + " Grant reason required : \n", + " Email : _@verily-bvdp.com\n", + " Current user's roles : ADMIN\n", "```" ] }, @@ -271,16 +286,32 @@ " self.label = widgets.Label(value='Please provide the name & org ID for your Verily Workbench group.')\n", " self.input_group_name = wu.TextInputWidget('',\"Group Name:\").get()\n", " self.input_org_id = wu.TextInputWidget('', \"Organization ID:\").get()\n", + " self.input_expiration_duration = wu.BoundIntInputWidget(0,0,366,'Expiration Duration in Days:').get()\n", + " self.enable_expiration_notification = wu.LongLabelCheckbox('Enable notifications of expiration to group admins.').get()\n", + " self.require_grant_reason = wu.LongLabelCheckbox('Require admins to provide a reason when granting a role on this group.').get()\n", " self.output = widgets.Output()\n", " self.button = wu.StyledButton('Create group','Click to create a Verily Workbench group.','plus').get()\n", - " self.vb = widgets.VBox([self.label,self.input_group_name,self.input_org_id,self.button,self.output],layout=wu.vbox_layout)\n", + " self.required_fields = [\n", + " self.label,self.input_group_name,self.input_org_id,self.input_expiration_duration,\n", + " self.enable_expiration_notification, self.require_grant_reason, self.button,self.output\n", + " ]\n", + " self.vb = widgets.VBox(children=self.required_fields,layout=wu.vbox_layout)\n", " self.button.on_click(self.create_group)\n", "\n", " def create_group(self,b):\n", " with self.output:\n", " self.output.clear_output()\n", - " wb_command = [\"wb\",\"group\",\"create\",f\"--name={self.input_group_name.value}\",f\"--org={self.input_org_id.value}\",\"--format=text\"]\n", - " result = subprocess.run(wb_command,capture_output=True,text=True)\n", + " commandList = [\"wb\",\"group\",\"create\",\n", + " f\"--name={self.input_group_name.value}\",\n", + " f\"--org={self.input_org_id.value}\",\n", + " f\"--expiration={int(self.input_expiration_duration.value)}\",\n", + " \"--format=text\"]\n", + "\n", + " if self.enable_expiration_notification.value:\n", + " commandList.append(f\"--expiration-notification\")\n", + " if self.require_grant_reason.value:\n", + " commandList.append(f\"--require-grant-reason\")\n", + " result = subprocess.run(commandList,capture_output=True,text=True)\n", " print(result.stderr) if not result.stdout else print(result.stdout)\n", "\n", "# Instantiate widget\n",