Getting Information on Project Collaborators

Overview

The Benchling API provides various broad endpoints for retrieving lists of different types of objects, such as users, apps, projects, and permissions access policies. This guide demonstrates how to leverage these endpoints in combination to fulfill the following example use cases:

  1. Getting information about collaborators on all projects on a tenant
  2. Getting all collaborators on a specific project
  3. Getting a specific party’s access policy on a specific project
  4. Getting a specific user’s access policies for all projects on which they are shared

1) Getting all collaborators on a specific project

  1. For a given project id (eg. src_TnTw2xzy), make a request to api/v2-alpha/projects/<project_id>/collaborations. This will return something like the following:
{
    "collaborations": [
        {
        "accessPolicy": {
            "id": "pol_12345", 
            "name": "READ"
            "policyStatements": [
                {
                    "access": "GRANTED", 
                    "description": "Projects and folders - View"
                }, 
                {
                    "access": "NOT_GRANTED", 
                    "description": "Projects and folders - Create folders"
                },
                ...
            ]
        },
        "collaborator": {
            "type": "USER",
            "handle": "albert.einstein", 
            "id": "ent_rCgpcKrj", 
            "name": "Albert Einstein"
        }
        },
        {
        "accessPolicy": {
            "id": "pol_23456", 
            "name": "WRITE",
            "policyStatements": [
                {
                    "access": "GRANTED", 
                    "description": "Projects and folders - View"
                },
                ...
            ]
        },        
        "collaborator": {
            "type": "TEAM",
            "role": "ADMIN",
            "handle": "benchlingteam", 
            "id": "team_oSrQDUmh", 
            "name": "Benchling Team"
        } 
        },
{
        "accessPolicy": {
            "id": "pol_23456", 
            "name": "READ",
            "policyStatements": [
                {
                    "access": "GRANTED", 
                    "description": "Projects and folders - View"
                },
                ...
            ]
        },        
        "collaborator": {
            "type": "APP",
            "handle": "", 
            "id": "app_ROpiaHWZUrrZFUxy", 
            "name": "Integration App"
        } 
        }
    ]
}

Going forward, let us define the above request by

get_project_collaborations(project_id): api/v2-alpha/projects/<project_id>/collaborations

This endpoint provides the same amount of information as the project collaborators UI (pictured below).

1186

Screenshot of the project permissions page

  1. We can further break down group collaborators into individual users by leveraging the /users endpoint. For example, to get all users that are admins of Benchling Team and have WRITE access in the above example, we can make a call to /api/v2-beta/users?adminOf=team_oSrQDUmh. This returns a list of users:
{
    "users": [
        {
           "handle": "lpasteur", 
            "id": "ent_a8asdp", 
            "name": "Louis Pasteur"
        }
    ]
}

We define the following requests:

get_team_admins(team_id): /api/v2-beta/users?adminOf=team_id
get_team_members(team_id): /api/v2-beta/users?memberOf=team_id
get_org_admins(org_id): /api/v2-beta/users?adminOf=org_id
get_org_members(org_id): /api/v2-beta/users?memberOf=org_id

Combining the results of these subqueries with the results of the previous request, we can create a full list of individual users and their access levels to each project on the tenant. In Python, this could look like the following:

results = []
projects = get_all_projects() 
for project in projects:
    collaborations = get_project_collaborations(project.id)
    for collaboration in collaborations:
        collaborator = collaboration.collaborator
        role, type = collaboration.collaborator.role, collaboration.collaborator.type
        
        # if collaboration is for a specific user, add directly to results
        if type == "USER":
            results.append({"project": project, "user": collaborator, "policy": collaboration.policy})
            
        # otherwise, get users corresponding to role
        elif type == "ORGANIZATION" and role == "ADMIN":
            org_admins = get_org_admins(collaborator.id)
            for org_admin in org_admins:
                results.append({"project": project, "user": org_admin, "policy": collaboration.policy})
        
        elif type == "ORGANIZATION" and role == "MEMBER":
            # similarly iterate through org_members
       
	elif collaborator.type == "TEAM" and role = "ADMIN":
            # iterate through team_admins
       
        elif collaborator.type == "TEAM" and role = "MEMBER":
            # iterate through team_members

πŸ“˜

Collaborators with Multiple Access Policies

In this and all the following cases, our API might return more than one access policy per user on a project, if the user gets different policies through memberships of different groups. Refer to Understanding Combined Access Policies to learn how to interpret multiple policies.

2) Getting information about collaborators on all projects on a tenant

  1. Make a request (or multiple requests, until nextToken is empty) to api/v2/projects to get a list of all projects on the tenant accessible to the API caller. This request, which we'll define as get_all_projects(), will return a paginated list with the following shape:
{
    "projects": [
        {
            "archiveRecord": null, 
            "id": "src_TnTw2xzy", 
            "name": "Martian DNA Project", 
            "owner": {
                "handle": "albert.einstein", 
                "id": "ent_rCgpcKrj", 
                "name": "Albert Einstein"
            }
        }, 
    ],
    "nextToken": ""
}

For each project, we can then reuse the logic from the previous use case to get information on its collaborators.

3) Getting a specific party’s access policy on a specific project

Similarly, to get a specific party’s access level on a specific project, we can reuse the logic from Use Case #1 to get collaborator info on the project. If our party of interest is a team or organization, we can simply go through the project’s collaborators and check for that group. If our party is a user, we can delve into the admins and members of each collaborating group and check to see if our user is present.

4) Getting a specific user’s access policy for all projects they can access

Next, we’ll walk through the process for getting a user’s policies on all accessible projects, including projects shared with teams or organizations of which they are members.
This could also be done by filtering the complete results from Use Case #2 for the desired user. However, we will use this opportunity to demonstrate the /teams and /organizations endpoints, which work in the opposite direction.

πŸ“˜

Listing Endpoint Visibility

All listing endpoints, including api/v2/projects, return only objects that are viewable by the API caller. In this use case, only projects accessible to both the given user and the API caller are returned.

  1. We first make requests to get all teams and orgs of which the user is a member or admin, using the HasMembers and HasAdmins filters. We can define functions to make the following calls:
get_member_teams(id): api/v2-beta/teams?hasMembers=id
get_admin_teams(id): api/v2-beta/teams?hasAdmins=id
get_member_orgs(id): api/v2-beta/organizations?hasMembers=id
get_admin_orgs(id): api/v2-beta/organizations?hasAdmins=id
  1. Next, we follow steps 1 and 2 from the previous section to retrieve all collaborations on all projects on the tenant, which once again gives us a list with the following shape:
{
    "collaborations": [
        {
            "accessPolicy": {
                "id": "datapol_12345", 
                "name": "READ",
                "policyStatements": [
                    {
                        "access": "GRANTED", 
                        "description": "Projects and folders - View"
                    },
                    ...
                ]
            },
            "collaborator": {
                "type": "USER",
                "handle": "albert.einstein", 
                "id": "ent_rCgpcKrj", 
                "name": "Albert Einstein"
            }
        },
        {
        "accessPolicy": {
            "id": "datapol_23456", 
            "name": "WRITE",
            "policyStatements": [
                {
                    "access": "GRANTED", 
                    "description": "Projects and folders - View"
                },
                ...
            ]
        },        
        "collaborator": {
            "type": "TEAM",
            "role": "ADMIN",
            "handle": "benchlingteam", 
            "id": "team_oSrQDUmh", 
            "name": "Benchling Team"
        } 
    ]
}
  1. For each project, we can now iterate through its collaborations, determine if any of those collaborations include our user or one of their groups, and get a list of corresponding policies the user has on the project. In Python, this might look something like the following:
member_team_ids = [team.id for team in get_member_teams("ent_a8asdp")]
# similarly define admin_team_ids, member_org_ids, admin_org_ids

results = []
for project in get_all_projects():
    for collaboration in get_project_collaborations(project.id):
        role, type = collaboration.collaborator.role, collaboration.collaborator.type

        if role == "ADMIN" and type == "TEAM":
            if collaborator.id in admin_team_ids:
                results.append({"project": project, "policy": collaboration.accessPolicy})
       
        if role == "MEMBER" and type == "TEAM":
            # append from member_team_ids
      
        if role == "ADMIN" and type == "ORG":
            # append from admin_org_ids
        
        if role == "MEMBER" and type == "ORG":
            # append from member_org_ids

Understanding Combined Access Policies

In this section, we explain how to determine a user’s combined permissions across multiple access policies.

Assume we’ve followed the steps for Use Case 3 above, giving us the following list of policies for a single user on a single project:

policies = [
    {
        "id": "datapol_12345", 
        "name": "LIBRARIAN"
        "policyStatements": [
            {
                "access": "GRANTED", 
                "description": "Projects and folders - View"
            }, 
            {
                "access": "GRANTED_TO_AUTHOR", 
                "description": "Projects and folders - Create folders"
            }, 
            {
                "access": "NOT_GRANTED", 
                "description": "Projects and folders - Add other items"
            }, 
            {
                "access": "NOT_GRANTED", 
                "description": "Projects and folders - Edit folder properties"
            },
            ...
        ]
    },
        
    {
        "id": "datapol_23456", 
        "name": "SCIENTIST", 
        "policyStatements": [
            {
                "access": "GRANTED", 
                "description": "Projects and folders - View"
            }, 
            {
                "access": "NOT_GRANTED", 
                "description": "Projects and folders - Create folders"
            }, 
            {
                "access": "GRANTED", 
                "description": "Projects and folders - Add other items"
            }, 
            {
                "access": "NOT_GRANTED", 
                "description": "Projects and folders - Edit folder properties"
            },
            ...
        ]
]

The right way to interpret the combined permissions for a user can be found by going through each the policy statements of each of the user’s access policies and taking the most permissive value of access, where GRANTED is the most permissive, GRANTED_TO_AUTHOR is the next most permissive, and NOT_GRANTED is the least permissive. Thus, in the above example, our user’s effective permissions are:

  • Projects and folders - View: Granted (by both Librarian and Scientist)
  • Projects and folders - Create folders: Granted to Author (by Librarian, which supersedes Scientist)
  • Projects and folders - Add other items: Granted (by Scientist, which supersedes Librarian)
  • Projects and folders - Edit folder properties: Not Granted (by either policy)

The Python code for this could look as follows:

from collections import defaultdict

for policy in policies:
   # create dict mapping each policy statement description to a set of 
   # access levels granted to the user across policies
    permission_levels_by_description = defaultdict(set)
    for statement in policy.policy_statements:
        permission_levels_by_description[statement.description].add(statement.access)

# get new effective policy statements dict with most permissive access 
# level for each policy statement
effective_policy_statements = []
for description, access_levels in permission_levels_by_description.items():
    if "GRANTED" in access_levels:
        access = "GRANTED"
    elif "GRANTED_TO_AUTHOR" in access_levels:
       access = "GRANTED_TO_AUTHOR"
    else:
       access = "NOT_GRANTED"
    effective_policy_statements.append(
        {
            "description": description,
            "access": access
        }
    )