Skip to content

Added shared account selection module based on the old python API #64

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

Merged
merged 1 commit into from
Oct 6, 2023
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
17 changes: 17 additions & 0 deletions examples/dev/dev_change_account_and_submit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import flow360 as fl
from flow360.examples import OM6wing

fl.Env.dev.active()

# choose shared account interactively
fl.Accounts.choose_shared_account()

# retrieve mesh files
OM6wing.get_files()

# submit mesh
volume_mesh = fl.VolumeMesh.from_file(OM6wing.mesh_filename, name="OM6wing-mesh")
volume_mesh = volume_mesh.submit()

# leave the account
fl.Accounts.leave_shared_account()
1 change: 1 addition & 0 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


from . import global_exception_handler
from .accounts_utils import Accounts
from .cli import flow360
from .cloud.s3_utils import ProgressCallbackInterface
from .component import meshing
Expand Down
166 changes: 166 additions & 0 deletions flow360/accounts_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""
This module provides utility functions for managing access between interconnected accounts.

Functions:
- choose_shared_account(None) -> None - select account from the list of client and organization accounts interactively
- choose_shared_account(email: str, optional) -> None - select account matching the provided email (if exists)
- shared_account_info(None) -> str - return current shared account email address (if exists, None otherwise)
- leave_shared_account(None) -> None - leave current shared account
"""

from requests import HTTPError

from flow360.cloud.http_util import http
from flow360.environment import Env
from flow360.log import log

from .exceptions import WebError


class AccountsUtils:
"""
Current account info and utility functions.
"""

def __init__(self):
self._current_email = None
self._current_user_identity = None
self._confirmed_submit = False

@staticmethod
def _interactive_selection(users):
print(
"Choosing account in interactive mode, please select email from the organization list: "
)

user_count = len(users)

for i in range(0, user_count):
print(f"{i}: {users[i]['userEmail']}")

while True:
try:
value = input(
f"Enter address of the account to switch to [0 - {user_count - 1}] or 'q' to abort: "
)
if value == "q":
return None
if int(value) in range(0, user_count):
return int(value)
print(f"Value out of range [0 - {user_count - 1}]")
continue
except ValueError:
print("Invalid input type, please input an integer value:")
continue

# Requires fixing from the backend side - support for portal webapi calls with apikey authentication
@staticmethod
def _get_supported_users():
try:
response = http.portal_api_get("auth")
access = response.json()["data"]
keys = access["user"]
supported_users = keys["guestUsers"]
if supported_users:
return supported_users
return []
except HTTPError as error:
raise WebError("Failed to retrieve supported user data from server") from error

@staticmethod
def _get_company_users():
try:
response = http.get("flow360/account")
company_users = response["tenantMembers"]
if company_users:
return company_users
return []
except HTTPError as error:
raise WebError("Failed to retrieve company user data from server") from error

def _check_state_consistency(self):
if Env.impersonate != self._current_user_identity:
log.warning(
(
f"Environment impersonation ({Env.impersonate}) does "
f"not match current account ({self._current_user_identity}), "
"this may be caused by explicit modification of impersonation "
"in the environment, use choose_shared_account() instead."
)
)
self._current_email = None
self._current_user_identity = None

def shared_account_submit_is_confirmed(self):
"""check if the user confirmed that he wants to submit resources to a shared account"""
return self._confirmed_submit

def shared_account_confirm_submit(self):
"""confirm submit for the current session"""
self._confirmed_submit = True

def choose_shared_account(self, email=None):
"""choose a shared account to impersonate

Parameters
----------
email : str, optional
user email to impersonate (if email exists among shared accounts),
if email is not provided user can select the account interactively
"""
shared_accounts = self._get_company_users()

if len(shared_accounts) == 0:
log.info("There are no accounts shared with the current user")
return

selected = None

addresses = [user["userEmail"] for user in shared_accounts]

if email is None:
selected = self._interactive_selection(shared_accounts)
elif email in addresses:
selected = addresses.index(email)

if selected is None:
raise ValueError("Invalid or empty email address selected, cannot change account.")

user_email = shared_accounts[selected]["userEmail"]
user_id = shared_accounts[selected]["userIdentity"]

Env.impersonate = user_id

self._confirmed_submit = False
self._current_email = user_email
self._current_user_identity = user_id

def shared_account_info(self):
"""
retrieve current shared account name, if possible
"""
self._check_state_consistency()

if self._current_email is not None:
log.info(f"Currently operating as {self._current_email}")
else:
log.info("Currently not logged into a shared account")

return self._current_email

def leave_shared_account(self):
Copy link
Collaborator

Choose a reason for hiding this comment

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

on leave, we should reset the submit confirm question

"""
leave current shared account name, if possible
"""
self._check_state_consistency()

if Env.impersonate is None:
log.warning("You are not currently logged into any shared account")
else:
log.info(f"Leaving shared account {self._current_email}")
self._current_email = None
self._current_user_identity = None
Env.impersonate = None


Accounts = AccountsUtils()
5 changes: 4 additions & 1 deletion flow360/component/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
before_submit_only,
is_object_cloud_resource,
)
from .utils import is_valid_uuid, validate_type
from .utils import is_valid_uuid, shared_account_confirm_proceed, validate_type
from .validator import Validator


Expand Down Expand Up @@ -241,6 +241,9 @@ def submit(self, force_submit: bool = False) -> Case:

self.validate_case_inputs(pre_submit_checks=True)

if not shared_account_confirm_proceed():
raise FlValueError("User aborted resource submit.")

volume_mesh_id = self.volume_mesh_id
parent_id = self.parent_id
if parent_id is not None:
Expand Down
5 changes: 4 additions & 1 deletion flow360/component/surface_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
Flow360ResourceListBase,
ResourceDraft,
)
from .utils import validate_type
from .utils import shared_account_confirm_proceed, validate_type
from .validator import Validator
from .volume_mesh import VolumeMeshDraft

Expand Down Expand Up @@ -121,6 +121,9 @@ def submit(self, progress_callback=None) -> SurfaceMesh:
if name is None:
name = os.path.splitext(os.path.basename(self.geometry_file))[0]

if not shared_account_confirm_proceed():
raise FlValueError("User aborted resource submit.")

data = {
"name": self.name,
"tags": self.tags,
Expand Down
27 changes: 27 additions & 0 deletions flow360/component/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

import zstandard as zstd

from ..accounts_utils import Accounts
from ..cloud.utils import _get_progress, _S3Action
from ..error_messages import shared_submit_warning
from ..exceptions import TypeError as FlTypeError
from ..exceptions import ValueError as FlValueError
from ..log import log
Expand Down Expand Up @@ -48,6 +50,31 @@ def wrapper_func(*args, **kwargs):
return wrapper


def shared_account_confirm_proceed():
"""
Prompts confirmation from user when submitting a resource from a shared account
"""
email = Accounts.shared_account_info()
if email is not None and not Accounts.shared_account_submit_is_confirmed():
log.warning(shared_submit_warning(email))
print("Are you sure you want to proceed? (y/n): ")
while True:
try:
value = input()
if value.lower() == "y":
Accounts.shared_account_confirm_submit()
return True
if value.lower() == "n":
return False
print("Enter a valid value (y/n): ")
continue
except ValueError:
print("Invalid input type")
continue
else:
return True


# pylint: disable=bare-except
def _get_value_or_none(callable):
try:
Expand Down
5 changes: 4 additions & 1 deletion flow360/component/volume_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
ResourceDraft,
)
from .types import COMMENTS
from .utils import validate_type, zstd_compress
from .utils import shared_account_confirm_proceed, validate_type, zstd_compress
from .validator import Validator

try:
Expand Down Expand Up @@ -503,6 +503,9 @@ def submit(self, progress_callback=None) -> VolumeMesh:
VolumeMesh object with id
"""

if not shared_account_confirm_proceed():
raise FlValueError("User aborted resource submit.")

if self.file_name is not None:
return self._submit_upload_mesh(progress_callback)

Expand Down
7 changes: 7 additions & 0 deletions flow360/error_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ def change_solver_version_error(from_version, to_version):
"""


def shared_submit_warning(email):
return f"""\
You are submitting a resource to a shared account {email}.
This message will not be shown again for this session if you confirm.
"""


def params_fetching_error(err_msg):
return f"""\
There was a problem when fetching params for this case
Expand Down
8 changes: 4 additions & 4 deletions flow360/user_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ def suppress_submit_warning(self):
"""locally suppress submit warning"""
self._suppress_submit_warning = True

def cancel_local_submit_warning_settings(self):
"""cancel local submit warning settings"""
self._suppress_submit_warning = None

def show_submit_warning(self):
"""locally show submit warning"""
self._suppress_submit_warning = False
Expand All @@ -100,6 +96,10 @@ def is_suppress_submit_warning(self):
return self._suppress_submit_warning
return self.config.get("user", {}).get("config", {}).get("suppress_submit_warning", False)

def cancel_local_submit_warning_settings(self):
"""cancel local submit warning settings"""
self._suppress_submit_warning = None

@property
def do_validation(self):
"""for handling user side validation (pydantic)
Expand Down
28 changes: 28 additions & 0 deletions tests/data/mock_webapi/organization_accounts_resp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"data": {
"credit": 100000000000.0,
"s3Usage": 0,
"totalTaskCount": 0,
"monthlyTaskCount": 0,
"creditExpiration": "2029-05-31T07:28:31.829Z",
"accountType": "tenant",
"tenantId": "0000000-0000-0000-0000-000000000000",
"tenantName": "Test",
"clientAdmin": true,
"allowanceCurrentCycleAmount": null,
"allowanceCurrentCycleTotalAmount": null,
"allowanceCurrentCycleStartDate": null,
"allowanceCurrentCycleEndDate": null,
"allowanceNextCycleStartDate": null,
"allowanceAllCycleStartDate": null,
"allowanceAllCycleEndDate": null,
"tenantMembers": [
{"userIdentity": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "userId": "AAAAAAAAAAAAAAAAAAAAA", "userEmail": "user1@test.com"},
{"userIdentity": "us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "userId": "BBBBBBBBBBBBBBBBBBBBB", "userEmail": "user2@test.com"}],
"intraCompanySharingEnabled": true,
"taskDeInfo": null,
"userId": "ABCDEFGHIJKLMNOPRSTUV",
"internal": true
}
}

11 changes: 11 additions & 0 deletions tests/mock_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ def json():
return res


class MockResponseOrganizationAccounts(MockResponse):
"""response to retrieving shared account list"""

@staticmethod
def json():
with open(os.path.join(here, "data/mock_webapi/organization_accounts_resp.json")) as fh:
res = json.load(fh)
return res


class MockResponseCase(MockResponse):
"""response if Case(id="00000000-0000-0000-0000-000000000000")"""

Expand Down Expand Up @@ -136,6 +146,7 @@ def json():
"/cases/00112233-4455-6677-8899-bbbbbbbbbbbb/runtimeParams": MockResponseCaseRuntimeParams,
"/cases/c58e7a75-e349-476a-9020-247af6b2e92b": MockResponseCase,
"-python-client-v2": MockResponseVersions,
"/account": MockResponseOrganizationAccounts,
}


Expand Down
Loading