From a61a600095aab5775760f5d2160e6f2156dd96cb Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Sat, 28 Jun 2025 19:12:21 +0200 Subject: [PATCH 1/3] added a quick fix but idea needs to be worked on --- src/vaultwardentools/client.py | 40 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/vaultwardentools/client.py b/src/vaultwardentools/client.py index 0eafa00..5f6b4b7 100644 --- a/src/vaultwardentools/client.py +++ b/src/vaultwardentools/client.py @@ -2485,16 +2485,21 @@ def get_user(self, email=None, name=None, id=None, user=None, sync=None): exc.criteria = criteria raise exc - def get_users_from_organization(self, orga, include_groups=False, token=None): + def get_users_from_organization(self, orga, include_groups=False,sync = False, token=None): token = self.get_token(token) orga = self.get_organization(orga, token=token) res = self.r(f"/api/organizations/{orga.id}/users" + ("?includeGroups=true" if include_groups else ""), method="get") - users = {} + users = {"id":{}, "email":{}} for user in res.json()["data"]: - users[user["id"]] = BWFactory.construct(user, client=self, unmarshall=True, ) + profile = BWFactory.construct(user, client=self, unmarshall=True, ) + users["id"][user["id"]] = user + users["email"][user["email"]] = user return users + def get_user_from_organization(self, orga, email, sync=False, token=None): + return self.get_users_from_organization(orga, include_groups=False, sync=sync, token=token)["email"][email] + def assert_bw_response( self, response, expected_status_codes=None, expected_callback=None, *a, **kw ): @@ -2540,21 +2545,21 @@ def post_user_request(self, resp, sync=True): def enable_user(self, email=None, name=None, id=None, user=None): user = self.get_user(email=email, name=name, id=id, user=user) - resp = self.adminr(f"/users/{user.id}/enable", headers={"Content-Type": "application/json"}) + resp = self.adminr(f"/users/{user.id}/enable", headers={"Content-Type":"application/json"}) self.post_user_request(resp) L.info(f"Enabled user {user.email} / {user.name} / {user.id}") return resp def disable_user(self, email=None, name=None, id=None, user=None): user = self.get_user(email=email, name=name, id=id, user=user) - resp = self.adminr(f"/users/{user.id}/disable", headers={"Content-Type": "application/json"}) + resp = self.adminr(f"/users/{user.id}/disable", headers={"Content-Type":"application/json"}) self.post_user_request(resp) L.info(f"Disabled user {user.email} / {user.name} / {user.id}") return resp def delete_user(self, email=None, name=None, id=None, user=None, sync=True, **kw): user = self.get_user(email=email, name=name, id=id, user=user, sync=sync) - resp = self.adminr(f"/users/{user.id}/delete", headers={"Content-Type": "application/json"}) + resp = self.adminr(f"/users/{user.id}/delete", headers={"Content-Type":"application/json"}) self.post_user_request(resp) self.uncache(obj=user, **kw) L.info(f"Deleted user {user.email} / {user.name} / {user.id}") @@ -3547,7 +3552,7 @@ def confirm_invitation(self, orga, email, name=None, sync=None, token=None): L.info(f"Confirmed user {email} / {user_id} in orga {orga.name} / {orga.id}") return acl - def create_group(self, group, orga, users=[], collections=[], token=None): + def create_group(self, orga, group, users=[], collections=[], token=None): v, i = self.version() if i and (v < API_CHANGES["1.27.0"]): raise WrongVersionOfServer(f"the server has version {v} and doesn't support groups") @@ -3566,15 +3571,13 @@ def create_group(self, group, orga, users=[], collections=[], token=None): d.load_single() return d - def get_groups(self, orga, sync=None, cache=None, token=None): + def get_groups(self, orga, sync=False, cache=None, token=None): v, i = self.version() if i and (v < API_CHANGES["1.27.0"]): raise WrongVersionOfServer(f"the server has version {v} and doesn't support groups") token = self.get_token(token=token) orga = self.get_organization(orga, token=token) _CACHE = self._cache["groups"] - if sync is None: - sync = False if cache is None: cache = True if cache is False or sync: @@ -3688,6 +3691,9 @@ def edit_group(self, orga=None, users=None, collections=None, + readonly=False, + hidepasswords=False, + manage=False, sync=None, token=None): v, i = self.version() if i and (v < API_CHANGES["1.27.0"]): @@ -3702,12 +3708,16 @@ def edit_group(self, payload = { "users": get_ids(users), "name": group.name, - "collections": [] + "collections": [], } - if len(collections) > 0 and isinstance(collections[0], Groupcollectiondetails): - payload["collections"] = [i.to_payload() for i in collections] - else: - payload["collections"] = collections + if collections: + orga = self.get_organization(group.organizationId, token=token, sync=sync) + dcollections = self.collections_to_payloads( + collections, orga=orga, token=token + ) + payload["collections"] = self.compute_accesses( + dcollections, readonly=readonly, hidepasswords=hidepasswords, manage=manage + )["payloads"] resp = self.r(f"/api/organizations/{group.organizationId}/groups/{_id}", json=payload, method="put", token=token) self.assert_bw_response(resp, expected_status_codes=[200, 500]) From 04e8f4155876d12ed068f4904808375aeafadc5a Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Sun, 29 Jun 2025 18:03:09 +0200 Subject: [PATCH 2/3] still not working but started to implement the cache to make less calls --- src/vaultwardentools/client.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/vaultwardentools/client.py b/src/vaultwardentools/client.py index 5f6b4b7..50f6941 100644 --- a/src/vaultwardentools/client.py +++ b/src/vaultwardentools/client.py @@ -1431,6 +1431,9 @@ def cache_collection(self, r, cache_key=SYNC_ALL_ORGAS_ID, **kw): r, cache=self._cache["collections"], cache_key=cache_key, **kw ) + def cache_organization_users(self, r, org_id, **kw): + return self._cache_objects(r, "organizations", cache=self._cache[org_id]["users"]) + def add_cipher(self, ret, obj, **kw): return self._cache_object(obj, cache=ret) @@ -2493,12 +2496,30 @@ def get_users_from_organization(self, orga, include_groups=False,sync = False, t users = {"id":{}, "email":{}} for user in res.json()["data"]: profile = BWFactory.construct(user, client=self, unmarshall=True, ) - users["id"][user["id"]] = user - users["email"][user["email"]] = user + users["id"][user["id"]] = profile + users["email"][user["email"]] = profile return users - def get_user_from_organization(self, orga, email, sync=False, token=None): - return self.get_users_from_organization(orga, include_groups=False, sync=sync, token=token)["email"][email] + def get_user_from_organization(self, orga, user, sync=False, token=None): + token = self.get_token(token) + if isinstance(user, Groupdetails): + if not sync: + return user + else: + self.get_users_from_organization(orga, token, sync) + _id = self.item_or_id(user) + orga = self.get_organization(orga, token=token) + try: + return self._cache["orga"]["id"][orga.id]["email"][_id] + except KeyError: + pass + try: + return self._cache["orga"]["id"][orga.id]["name"][_id] + except KeyError: + pass + exc = OrganizationNotFound(f"No such user found {email}") + exc.criteria = [orga] + raise exc def assert_bw_response( self, response, expected_status_codes=None, expected_callback=None, *a, **kw From 30ac615db000035a0edfa933b3994f972810c454 Mon Sep 17 00:00:00 2001 From: JunkyDeveloper Date: Mon, 30 Jun 2025 03:53:08 +0200 Subject: [PATCH 3/3] get_user_from_organization works with cache so reduced calls --- src/vaultwardentools/client.py | 77 ++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/vaultwardentools/client.py b/src/vaultwardentools/client.py index 50f6941..b464eb4 100644 --- a/src/vaultwardentools/client.py +++ b/src/vaultwardentools/client.py @@ -40,6 +40,7 @@ "groups": {"sync": False, SYNC_ALL_GROUPS_ID: deepcopy(DEFAULT_CACHE)}, "users": deepcopy(DEFAULT_CACHE), "organizations": deepcopy(DEFAULT_CACHE), + "organization_users": {}, "collections": {"sync": False, SYNC_ALL_ORGAS_ID: deepcopy(DEFAULT_CACHE)}, "ciphers": { "sync": False, @@ -471,6 +472,7 @@ def __init__( vaultiersecretid=None, unmarshall=False, ): + self.vaultiersecretid = None if unmarshall: jsond = unmarshall_value(jsond) self._client = client @@ -649,9 +651,9 @@ class Organization(BWFactory): """.""" def __init__(self, *a, **kw): - ret = super(Organization, self).__init__(*a, **kw) + self.name = None + super(Organization, self).__init__(*a, **kw) self._complete = False - return ret class Organizationuseruserdetails(BWFactory): @@ -660,6 +662,10 @@ class Organizationuseruserdetails(BWFactory): class Groupcollectiondetails(BWFactory): """.""" + id: str + hidePasswords: bool + readOnly: bool + manage: bool def to_payload(self): return { @@ -672,6 +678,7 @@ def to_payload(self): class Groupdetails(BWFactory): """.""" + name: str def load_single(self, jsond=None): super(Groupdetails, self).load(jsond) @@ -775,6 +782,7 @@ class Collection(BWFactory): """.""" def __init__(self, *a, **kw): + self.organizationId = None BWFactory.__init__(self, *a, **kw) self.externalId = getattr(self, "externalId", None) self._orga = None @@ -794,6 +802,7 @@ class Collectiondetails(BWFactory): """.""" def __init__(self, *a, **kw): + self.organizationId = None BWFactory.__init__(self, *a, **kw) self.externalId = getattr(self, "externalId", None) self._orga = None @@ -1427,12 +1436,10 @@ def cache_group(self, r, cache_key=SYNC_ALL_GROUPS_ID, **kw): return self._cache_objects(r, cache=self._cache["groups"], cache_key=cache_key, uniques=["id"], **kw) def cache_collection(self, r, cache_key=SYNC_ALL_ORGAS_ID, **kw): - return self._cache_objects( - r, cache=self._cache["collections"], cache_key=cache_key, **kw - ) + return self._cache_objects(r, cache=self._cache["collections"], cache_key=cache_key, **kw) def cache_organization_users(self, r, org_id, **kw): - return self._cache_objects(r, "organizations", cache=self._cache[org_id]["users"]) + return self._cache_objects(r, org_id, cache=self._cache["organization_users"], uniques=["id", "email"], **kw) def add_cipher(self, ret, obj, **kw): return self._cache_object(obj, cache=ret) @@ -2488,21 +2495,18 @@ def get_user(self, email=None, name=None, id=None, user=None, sync=None): exc.criteria = criteria raise exc - def get_users_from_organization(self, orga, include_groups=False,sync = False, token=None): + def get_users_from_organization(self, orga, include_groups=False, sync=False, token=None): token = self.get_token(token) orga = self.get_organization(orga, token=token) res = self.r(f"/api/organizations/{orga.id}/users" + ("?includeGroups=true" if include_groups else ""), method="get") - users = {"id":{}, "email":{}} for user in res.json()["data"]: - profile = BWFactory.construct(user, client=self, unmarshall=True, ) - users["id"][user["id"]] = profile - users["email"][user["email"]] = profile - return users + self.cache_organization_users(BWFactory.construct(user, client=self, unmarshall=True, ), orga.id) + return self.cache_organization_users([], orga.id) def get_user_from_organization(self, orga, user, sync=False, token=None): token = self.get_token(token) - if isinstance(user, Groupdetails): + if isinstance(user, Organizationuseruserdetails): if not sync: return user else: @@ -2510,14 +2514,26 @@ def get_user_from_organization(self, orga, user, sync=False, token=None): _id = self.item_or_id(user) orga = self.get_organization(orga, token=token) try: - return self._cache["orga"]["id"][orga.id]["email"][_id] + cache = self.cache_organization_users([], orga.id) + except KeyError: + cache = self.get_users_from_organization(orga, token, sync) + try: + return cache["id"][_id] + except KeyError: + pass + try: + return cache["email"][_id] + except KeyError: + users = self.get_users_from_organization(orga, token, sync) + try: + return users["id"][_id] except KeyError: pass try: - return self._cache["orga"]["id"][orga.id]["name"][_id] + return users["email"][_id] except KeyError: pass - exc = OrganizationNotFound(f"No such user found {email}") + exc = OrganizationNotFound(f"No such user found {user}") exc.criteria = [orga] raise exc @@ -2566,21 +2582,21 @@ def post_user_request(self, resp, sync=True): def enable_user(self, email=None, name=None, id=None, user=None): user = self.get_user(email=email, name=name, id=id, user=user) - resp = self.adminr(f"/users/{user.id}/enable", headers={"Content-Type":"application/json"}) + resp = self.adminr(f"/users/{user.id}/enable", headers={"Content-Type": "application/json"}) self.post_user_request(resp) L.info(f"Enabled user {user.email} / {user.name} / {user.id}") return resp def disable_user(self, email=None, name=None, id=None, user=None): user = self.get_user(email=email, name=name, id=id, user=user) - resp = self.adminr(f"/users/{user.id}/disable", headers={"Content-Type":"application/json"}) + resp = self.adminr(f"/users/{user.id}/disable", headers={"Content-Type": "application/json"}) self.post_user_request(resp) L.info(f"Disabled user {user.email} / {user.name} / {user.id}") return resp def delete_user(self, email=None, name=None, id=None, user=None, sync=True, **kw): user = self.get_user(email=email, name=name, id=id, user=user, sync=sync) - resp = self.adminr(f"/users/{user.id}/delete", headers={"Content-Type":"application/json"}) + resp = self.adminr(f"/users/{user.id}/delete", headers={"Content-Type": "application/json"}) self.post_user_request(resp) self.uncache(obj=user, **kw) L.info(f"Deleted user {user.email} / {user.name} / {user.id}") @@ -3704,7 +3720,7 @@ def get_users_from_group(self, group, orga=None, sync=None, token=None): orga = self.get_organization(group.organizationId, token=token) users_org = self.get_users_from_organization(orga, token=token) for user_id in resp.json(): - users[user_id] = deepcopy(users_org[user_id]) + users[user_id] = deepcopy(users_org["id"][user_id]) return users def edit_group(self, @@ -3732,13 +3748,20 @@ def edit_group(self, "collections": [], } if collections: - orga = self.get_organization(group.organizationId, token=token, sync=sync) - dcollections = self.collections_to_payloads( - collections, orga=orga, token=token - ) - payload["collections"] = self.compute_accesses( - dcollections, readonly=readonly, hidepasswords=hidepasswords, manage=manage - )["payloads"] + if not isinstance(collections, list) and not isinstance(collections, str): + collections = [collections] + if isinstance(collections[0], Groupcollectiondetails): + payload["collections"] = [col.to_payload() for col in collections] + elif isinstance(collections, str): + payload["collections"] = collections + else: + orga = self.get_organization(group.organizationId, token=token, sync=sync) + dcollections = self.collections_to_payloads( + collections, orga=orga, token=token + ) + payload["collections"] = self.compute_accesses( + dcollections, readonly=readonly, hidepasswords=hidepasswords, manage=manage + )["payloads"] resp = self.r(f"/api/organizations/{group.organizationId}/groups/{_id}", json=payload, method="put", token=token) self.assert_bw_response(resp, expected_status_codes=[200, 500])