Skip to content

Commit

Permalink
Merge pull request #373 from mesozoic/grant_revoke_admin
Browse files Browse the repository at this point in the history
Add support for grant/revoke admin access
  • Loading branch information
mesozoic authored May 16, 2024
2 parents 9eba0f3 + 4907ada commit ca4237c
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 8 deletions.
12 changes: 12 additions & 0 deletions docs/source/enterprise.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,15 @@ via the following methods.
`Delete users by email <https://airtable.com/developers/web/api/delete-users-by-email>`__

>>> enterprise.delete_users(["[email protected]", "[email protected]"])

`Grant admin access <https://airtable.com/developers/web/api/grant-admin-access>`__

>>> enterprise.grant_admin("usrUserId")
>>> enterprise.grant_admin("[email protected]")
>>> enterprise.grant_admin(enterprise.user("usrUserId"))

`Revoke admin access <https://airtable.com/developers/web/api/revoke-admin-access>`__

>>> enterprise.revoke_admin("usrUserId")
>>> enterprise.revoke_admin("[email protected]")
>>> enterprise.revoke_admin(enterprise.user("usrUserId"))
12 changes: 12 additions & 0 deletions docs/source/migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ The 3.0 release has changed the API for retrieving ORM model configuration:
* - ``Model._get_meta(name)``
- ``Model.meta.get(name)``

Miscellaneous name changes
---------------------------------------------

.. list-table::
:header-rows: 1

* - Old name
- New name
* - :class:`~pyairtable.api.enterprise.ClaimUsersResponse`
- :class:`~pyairtable.api.enterprise.ManageUsersResponse`


Migrating from 2.2 to 2.3
============================

Expand Down
50 changes: 44 additions & 6 deletions pyairtable/api/enterprise.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
from typing import Any, Dict, Iterable, Iterator, List, Literal, Optional, Union

from pyairtable._compat import pydantic
from pyairtable.models._base import AirtableModel, update_forward_refs
from pyairtable.models.audit import AuditLogResponse
from pyairtable.models.schema import EnterpriseInfo, UserGroup, UserInfo
Expand Down Expand Up @@ -253,7 +254,7 @@ def remove_user(

def claim_users(
self, users: Dict[str, Literal["managed", "unmanaged"]]
) -> "ClaimUsersResponse":
) -> "ManageUsersResponse":
"""
Batch manage organizations enterprise account users. This endpoint allows you
to change a user's membership status from being unmanaged to being an
Expand All @@ -276,7 +277,7 @@ def claim_users(
]
}
response = self.api.post(f"{self.url}/users/claim", json=payload)
return ClaimUsersResponse.from_api(response, self.api, context=self)
return ManageUsersResponse.from_api(response, self.api, context=self)

def delete_users(self, emails: Iterable[str]) -> "DeleteUsersResponse":
"""
Expand All @@ -288,6 +289,41 @@ def delete_users(self, emails: Iterable[str]) -> "DeleteUsersResponse":
response = self.api.delete(f"{self.url}/users", params={"email": list(emails)})
return DeleteUsersResponse.from_api(response, self.api, context=self)

def grant_admin(self, *users: Union[str, UserInfo]) -> "ManageUsersResponse":
"""
Grant admin access to one or more users.
Args:
users: One or more user IDs, email addresses, or instances of
:class:`~pyairtable.models.schema.UserInfo`.
"""
return self._post_admin_access("grant", users)

def revoke_admin(self, *users: Union[str, UserInfo]) -> "ManageUsersResponse":
"""
Revoke admin access to one or more users.
Args:
users: One or more user IDs, email addresses, or instances of
:class:`~pyairtable.models.schema.UserInfo`.
"""
return self._post_admin_access("revoke", users)

def _post_admin_access(
self, action: str, users: Iterable[Union[str, UserInfo]]
) -> "ManageUsersResponse":
response = self.api.post(
f"{self.url}/users/{action}AdminAccess",
json={
"users": [
{"email": user_id} if "@" in user_id else {"id": user_id}
for user in users
for user_id in [user.id if isinstance(user, UserInfo) else user]
]
},
)
return ManageUsersResponse.from_api(response, self.api, context=self)


class UserRemoved(AirtableModel):
"""
Expand Down Expand Up @@ -352,13 +388,15 @@ class Error(AirtableModel):
message: Optional[str] = None


class ClaimUsersResponse(AirtableModel):
class ManageUsersResponse(AirtableModel):
"""
Returned from the `Manage user membership <https://airtable.com/developers/web/api/manage-user-membership>`__
endpoint.
Returned from the `Manage user membership <https://airtable.com/developers/web/api/manage-user-membership>`__,
`Grant admin access <https://airtable.com/developers/web/api/grant-admin-access>`__, and
`Revoke admin access <https://airtable.com/developers/web/api/revoke-admin-access>`__
endpoints.
"""

errors: List["ClaimUsersResponse.Error"]
errors: List["ManageUsersResponse.Error"] = pydantic.Field(default_factory=list)

class Error(AirtableModel):
id: Optional[str] = None
Expand Down
2 changes: 2 additions & 0 deletions pyairtable/models/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ class UserInfo(
enterprise_user_type: Optional[str]
invited_to_airtable_by_user_id: Optional[str]
is_managed: bool = False
is_admin: bool = False
is_super_admin: bool = False
groups: List[NestedId] = _FL()
collaborations: "Collaborations" = pydantic.Field(default_factory=Collaborations)

Expand Down
25 changes: 23 additions & 2 deletions tests/test_api_enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import pytest

from pyairtable.api.enterprise import (
ClaimUsersResponse,
DeleteUsersResponse,
Enterprise,
ManageUsersResponse,
)
from pyairtable.models.schema import EnterpriseInfo, UserGroup, UserInfo
from pyairtable.testing import fake_id
Expand Down Expand Up @@ -281,7 +281,7 @@ def test_claim_users(enterprise, enterprise_mocks):
"[email protected]": "unmanaged",
}
)
assert isinstance(result, ClaimUsersResponse)
assert isinstance(result, ManageUsersResponse)
assert enterprise_mocks.claim_users.call_count == 1
assert enterprise_mocks.claim_users.last_request.json() == {
"users": [
Expand Down Expand Up @@ -310,3 +310,24 @@ def test_delete_users(enterprise, requests_mock):
assert isinstance(parsed, DeleteUsersResponse)
assert parsed.deleted_users[0].email == "[email protected]"
assert parsed.errors[0].type == "INVALID_PERMISSIONS"


@pytest.mark.parametrize("action", ["grant", "revoke"])
def test_manage_admin_access(enterprise, enterprise_mocks, requests_mock, action):
user = enterprise.user(enterprise_mocks.user_id)
m = requests_mock.post(f"{enterprise.url}/users/{action}AdminAccess", json={})
method = getattr(enterprise, f"{action}_admin")
result = method(
fake_user_id := fake_id("usr"),
fake_email := "[email protected]",
user,
)
assert isinstance(result, ManageUsersResponse)
assert m.call_count == 1
assert m.last_request.json() == {
"users": [
{"id": fake_user_id},
{"email": fake_email},
{"id": user.id},
]
}

0 comments on commit ca4237c

Please sign in to comment.