Skip to content

Commit

Permalink
Implement AuthClient.create_project
Browse files Browse the repository at this point in the history
  • Loading branch information
sirosen committed Jul 5, 2023
1 parent 9ecb9f9 commit 0e4c62c
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 1 deletion.
5 changes: 5 additions & 0 deletions changelog.d/20230628_152745_sirosen_add_project_crud.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added
~~~~~

- Add ``AuthClient.create_project`` as a method for creating a new Project via
the Globus Auth Developer API (:pr:`NUMBER`)
71 changes: 71 additions & 0 deletions src/globus_sdk/_testing/data/auth/create_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import uuid

from globus_sdk._testing.models import RegisteredResponse, ResponseSet

project_id = str(uuid.uuid1())
star_lord = {
"identity_provider": str(uuid.uuid1()),
"identity_type": "login",
"organization": "Guardians of the Galaxy",
"status": "used",
"id": str(uuid.uuid1()),
"name": "Star Lord",
"username": "star.lord@guardians.galaxy",
"email": "star.lord2@guardians.galaxy",
}
guardians_group = {
"id": str(uuid.uuid1()),
"name": "Guardians of the Galaxy",
"description": "A group of heroes",
"organization": "Guardians of the Galaxy",
}


RESPONSES = ResponseSet(
default=RegisteredResponse(
service="auth",
path="/v2/api/projects",
method="POST",
json={
"project": {
"contact_email": "support@globus.org",
"id": project_id,
"admins": {
"identities": [star_lord],
"groups": [],
},
"project_name": "Guardians of the Galaxy",
"admin_ids": [star_lord["id"]],
"admin_group_ids": None,
"display_name": "Guardians of the Galaxy",
}
},
metadata={
"id": project_id,
"admin_id": star_lord["id"],
},
),
admin_group=RegisteredResponse(
service="auth",
path="/v2/api/projects",
method="POST",
json={
"project": {
"contact_email": "support@globus.org",
"id": project_id,
"admins": {
"identities": [],
"groups": [guardians_group],
},
"project_name": "Guardians of the Galaxy",
"admin_ids": None,
"admin_group_ids": [guardians_group["id"]],
"display_name": "Guardians of the Galaxy",
}
},
metadata={
"id": project_id,
"admin_group_id": guardians_group["id"],
},
),
)
72 changes: 72 additions & 0 deletions src/globus_sdk/services/auth/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@ def get_identity_providers(
self.get("/v2/api/identity_providers", query_params=query_params)
)

#
# Developer APIs
#

def get_projects(self) -> IterableResponse:
"""
Look up projects on which the authenticated user is an admin.
Expand Down Expand Up @@ -370,6 +374,74 @@ def get_projects(self) -> IterableResponse:
""" # noqa: E501
return GetProjectsResponse(self.get("/v2/api/projects"))

def create_project(
self,
display_name: str,
contact_email: str,
admin_ids: UUIDLike | t.Iterable[UUIDLike] | None = None,
admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | None = None,
) -> GlobusHTTPResponse:
"""
Create a new project. Requires the ``manage_projects`` scope.
At least one of ``admin_ids`` or ``admin_group_ids`` must be provided.
:param display_name: The name of the project
:type display_name: str
:param contact_email: The email address of the project's point of contact
:type contact_email: str
:param admin_ids: A list of user IDs to be added as admins of the project
:type admin_ids: str or uuid or iterable of str or uuid, optional
:param admin_group_ids: A list of group IDs to be added as admins of the project
:type admin_group_ids: str or uuid or iterable of str or uuid, optional
.. tab-set::
.. tab-item:: Example Usage
When creating a project, your account is not necessarily included as an
admin. The following snippet uses the ``manage_projects`` scope as well
as the ``openid`` and ``email`` scopes to get the current user ID and
email address and use those data to setup the project.
.. code-block:: pycon
>>> ac = globus_sdk.AuthClient(...)
>>> userinfo = ac.oauth2_userinfo()
>>> identity_id = userinfo["sub"]
>>> email = userinfo["email"]
>>> r = ac.create_project(
... "My Project",
... contact_email=email,
... admin_ids=identity_id,
... )
>>> project_id = r["project"]["id"]
.. tab-item:: Example Response Data
.. expandtestfixture:: auth.create_project
.. tab-item:: API Info
``POST /v2/api/projects``
.. extdoclink:: Create Project
:ref: auth/reference/#create_project
"""
body: dict[str, t.Any] = {
"display_name": display_name,
"contact_email": contact_email,
}
if admin_ids is not None:
body["admin_ids"] = list(utils.safe_strseq_iter(admin_ids))
if admin_group_ids is not None:
body["admin_group_ids"] = list(utils.safe_strseq_iter(admin_group_ids))
return self.post("/v2/api/projects", data={"project": body})

#
# OAuth2 Behaviors & APIs
#

def oauth2_get_authorize_url(
self, *, query_params: dict[str, t.Any] | None = None
) -> str:
Expand Down
6 changes: 5 additions & 1 deletion src/globus_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import sys
import typing as t
import uuid
from base64 import b64encode
from enum import Enum

Expand Down Expand Up @@ -50,14 +51,17 @@ def safe_strseq_iter(
Given an Iterable (typically of strings), produce an iterator over it of strings.
This is a passthrough with two caveats:
- if the value is a solitary string, yield only that value
- str value in the iterable which is not a string
- if the value is a solitary UUID, yield only that value (as a string)
- str values in the iterable which are not strings
This helps handle cases where a string is passed to a function expecting an iterable
of strings, as well as cases where an iterable of UUID objects is accepted for a
list of IDs, or something similar.
"""
if isinstance(value, str):
yield value
elif isinstance(value, uuid.UUID):
yield str(value)
else:
for x in value:
yield str(x)
Expand Down
72 changes: 72 additions & 0 deletions tests/functional/services/auth/base/test_create_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import json
import uuid

import pytest

from globus_sdk._testing import get_last_request, load_response


@pytest.mark.parametrize(
"admin_id_style", ("string", "list", "set", "uuid", "uuid_list")
)
def test_create_project_admin_id_styles(client, admin_id_style):
meta = load_response(client.create_project).metadata

if admin_id_style == "string":
admin_ids = meta["admin_id"]
elif admin_id_style == "list":
admin_ids = [meta["admin_id"]]
elif admin_id_style == "set":
admin_ids = {meta["admin_id"]}
elif admin_id_style == "uuid":
admin_ids = uuid.UUID(meta["admin_id"])
elif admin_id_style == "uuid_list":
admin_ids = [uuid.UUID(meta["admin_id"])]
else:
raise NotImplementedError(f"unknown admin_id_style {admin_id_style}")

res = client.create_project("My Project", "support@globus.org", admin_ids=admin_ids)

assert res["project"]["id"] == meta["id"]

last_req = get_last_request()
data = json.loads(last_req.body)
assert list(data) == ["project"], data # 'project' is the only key
assert data["project"]["display_name"] == "My Project", data
assert data["project"]["contact_email"] == "support@globus.org", data
assert data["project"]["admin_ids"] == [meta["admin_id"]], data
assert "admin_group_ids" not in data["project"], data


@pytest.mark.parametrize(
"group_id_style", ("string", "list", "set", "uuid", "uuid_list")
)
def test_create_project_group_id_styles(client, group_id_style):
meta = load_response(client.create_project, case="admin_group").metadata

if group_id_style == "string":
group_ids = meta["admin_group_id"]
elif group_id_style == "list":
group_ids = [meta["admin_group_id"]]
elif group_id_style == "set":
group_ids = {meta["admin_group_id"]}
elif group_id_style == "uuid":
group_ids = uuid.UUID(meta["admin_group_id"])
elif group_id_style == "uuid_list":
group_ids = [uuid.UUID(meta["admin_group_id"])]
else:
raise NotImplementedError(f"unknown group_id_style {group_id_style}")

res = client.create_project(
"My Project", "support@globus.org", admin_group_ids=group_ids
)

assert res["project"]["id"] == meta["id"]

last_req = get_last_request()
data = json.loads(last_req.body)
assert list(data) == ["project"], data # 'project' is the only key
assert data["project"]["display_name"] == "My Project", data
assert data["project"]["contact_email"] == "support@globus.org", data
assert data["project"]["admin_group_ids"] == [meta["admin_group_id"]], data
assert "admin_ids" not in data["project"], data

0 comments on commit 0e4c62c

Please sign in to comment.