Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds user.lock() and user.unlock() #123

Merged
merged 2 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from requests import Response, Session
from typing import Optional

from . import hooks, urls
from . import hooks, me, urls

from .auth import Auth
from .config import Config
Expand Down Expand Up @@ -48,9 +48,7 @@ def connect_version(self):

@property
def me(self) -> User:
url = urls.append_path(self.config.url, "v1/user")
response = self.session.get(url)
return User(self.config, self.session, **response.json())
return me.get(self.config, self.session)

@property
def oauth(self) -> OAuthIntegration:
Expand Down
22 changes: 22 additions & 0 deletions src/posit/connect/me.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import requests

from . import urls

from .config import Config
from .users import User


def get(config: Config, session: requests.Session) -> User:
"""
Gets the current user.

Args:
config (Config): The configuration object containing the URL.
session (requests.Session): The session object used for making HTTP requests.

Returns:
User: The current user.
"""
url = urls.append_path(config.url, "v1/user")
response = session.get(url)
return User(config, session, **response.json())
41 changes: 37 additions & 4 deletions src/posit/connect/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import requests


from . import urls
from . import me, urls

from .config import Config
from .paginator import Paginator
Expand Down Expand Up @@ -57,6 +57,40 @@ def confirmed(self) -> bool:
def locked(self) -> bool:
return self.get("locked") # type: ignore

def lock(self, *, force: bool = False):
"""
Locks the user account.

.. warning:: You cannot unlock your own account. Once an account is locked, only an admin can unlock it.
tdstein marked this conversation as resolved.
Show resolved Hide resolved

Args:
force (bool, optional): If set to True, overrides lock protection allowing a user to lock their own account. Defaults to False.

Returns:
None
"""
_me = me.get(self.config, self.session)
if _me.guid == self.guid and not force:
raise RuntimeError(
"You cannot lock your own account. Set force=True to override this behavior."
)
url = urls.append_path(self.config.url, f"v1/users/{self.guid}/lock")
body = {"locked": True}
self.session.post(url, json=body)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming this does not return the updated User record? That's too bad because updated_time should also be affected by this action. Not a huge deal but just wanted to note.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the 200 response body is empty for v1/users/{self.guid}/lock. Adding a subsequent refresh here seems reasonable though.

url = urls.append_path(self.config.url, f"v1/users/{self.guid}")
response = self.session.get(url)
self.update(**response.json())

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related: #141

super().update(locked=True)

def unlock(self):
"""
Unlocks the user account.

Returns:
None
"""
url = urls.append_path(self.config.url, f"v1/users/{self.guid}/lock")
body = {"locked": False}
self.session.post(url, json=body)
super().update(locked=False)

def _update(self, body):
if len(body) == 0:
return
Expand Down Expand Up @@ -126,13 +160,12 @@ def find_one(self) -> User | None:
return next(users, None)

def get(self, id: str) -> User:
url = urls.append_path(self.url, id)
url = urls.append_path(self.config.url, f"v1/users/{id}")
response = self.session.get(url)
raw_user = response.json()
return User(
config=self.config,
session=self.session,
**raw_user,
**response.json(),
)

def create(self) -> User:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"email": "[email protected]",
"username": "random_username",
"first_name": "Random",
"last_name": "User",
"user_role": "admin",
"created_time": "2022-01-01T00:00:00Z",
"updated_time": "2022-03-15T12:34:56Z",
"active_time": "2022-02-28T18:30:00Z",
"confirmed": false,
"locked": true,
"guid": "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6"
}
81 changes: 81 additions & 0 deletions tests/posit/connect/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,87 @@ def test_locked(self):
user = User(session, url, locked=False)
assert user.locked is False

@responses.activate
def test_lock(self):
responses.get(
"https://connect.example/__api__/v1/users/a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6",
json=load_mock("v1/users/a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6.json"),
)
c = Client(api_key="12345", url="https://connect.example/")
user = c.users.get("a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6")
assert user.guid == "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6"

responses.get(
"https://connect.example/__api__/v1/user",
json=load_mock("v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"),
)
responses.post(
"https://connect.example/__api__/v1/users/a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6/lock",
match=[responses.matchers.json_params_matcher({"locked": True})],
)
user.lock()
assert user.locked

@responses.activate
def test_lock_self_true(self):
responses.get(
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4",
json=load_mock("v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"),
)
c = Client(api_key="12345", url="https://connect.example/")
user = c.users.get("20a79ce3-6e87-4522-9faf-be24228800a4")
assert user.guid == "20a79ce3-6e87-4522-9faf-be24228800a4"

responses.get(
"https://connect.example/__api__/v1/user",
json=load_mock("v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"),
)
responses.post(
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4/lock",
match=[responses.matchers.json_params_matcher({"locked": True})],
)
user.lock(force=True)
assert user.locked

@responses.activate
def test_lock_self_false(self):
responses.get(
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4",
json=load_mock("v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"),
)
c = Client(api_key="12345", url="https://connect.example/")
user = c.users.get("20a79ce3-6e87-4522-9faf-be24228800a4")
assert user.guid == "20a79ce3-6e87-4522-9faf-be24228800a4"

responses.get(
"https://connect.example/__api__/v1/user",
json=load_mock("v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"),
)
responses.post(
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4/lock",
match=[responses.matchers.json_params_matcher({"locked": True})],
)
with pytest.raises(RuntimeError):
user.lock(force=False)
assert not user.locked

@responses.activate
def test_unlock(self):
responses.get(
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4",
json=load_mock("v1/users/20a79ce3-6e87-4522-9faf-be24228800a4.json"),
)
c = Client(api_key="12345", url="https://connect.example/")
user = c.users.get("20a79ce3-6e87-4522-9faf-be24228800a4")
assert user.guid == "20a79ce3-6e87-4522-9faf-be24228800a4"

responses.post(
"https://connect.example/__api__/v1/users/20a79ce3-6e87-4522-9faf-be24228800a4/lock",
match=[responses.matchers.json_params_matcher({"locked": False})],
)
user.unlock()
assert not user.locked


class TestUsers:
@responses.activate
Expand Down
Loading