Skip to content

Commit

Permalink
Merge pull request #32 from safepost/dev
Browse files Browse the repository at this point in the history
v0.1.8.2
  • Loading branch information
safepost authored Nov 13, 2024
2 parents 91fe0bc + 2cdafe7 commit d384532
Show file tree
Hide file tree
Showing 39 changed files with 3,803 additions and 778 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/venv/
/bastion_safepost.egg-info/
/*.egg-info/
/build/
/dist/
/.pypirc
Expand Down
74 changes: 73 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,81 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.8] - 2024-10-01
### Changes
- Added a function to get user's safes
- Added list members linked function
- Added the connection_component_by_platform function in utilities
- Changed the return of connection_component_usage from string to list of dict
- Adding PrivilegedAccount object directly in epv class to allow user to import it

### Bugfixes
- Added a semaphore in the move function to avoid mass creation before mass deletion (we ensure that max 50 accounts are created before the old accounts are deleted)

## [0.1.7] - 2024-08-01
### Changes
Configutation file changes:
- Existing section
- "AIM" section
- Added "passphrase" field in "AIM" section to handle AIM PEM encrypted certificate.

- New section:
- "accounts" section defined the EPV.account option fields:
handle "LOGON_ACCOUNT_INDEX" and "RECONCILE_ACCOUNT_INDEX" definition.
Those definition has been moved from the 'custom' section.

- "safe" section defined the EPV.safe option fields:
handle "cpm" and "retention" definition.
Those definition has been moved from the global section.

- "api_options" section defined the new global API options.
- "deprecated_warning" field to enable/disable deprecated warning (default is true = enable)

Serialization changes:
- Existing keys:
- "AIM" (dictionary)
- Added "passphrase" field in "AIM" section to handle AIM encrypted certificate.
- Default value for "verify" field is now "True" (ROOT certificate authority (CA) will be validated by default).

- New keys dictionary in serialization:
- "account" key (dictionary):
- "LOGON_ACCOUNT_INDEX" and "RECONCILE_ACCOUNT_INDEX" fields.
- "safe" key (dictionary):
- "cpm" and "retention" fields.
- "api_options" key (dictionary):
- "deprecated_warning" field.

- keys moved:
- from the global section to "account"
- "LOGON_ACCOUNT_INDEX" and "RECONCILE_ACCOUNT_INDEX" fields.
- from the "custom" section to "safe"
- "cpm" and "retention" fields.


Functions changes:
- Safe class:
- Added an "update" function

- EPV class:
- "login_with_aim" function has been modified to force some parameters to be specified in the form key=value.
For example: epv_env.login_with_aim(passphrase="Hello123Word!")
- "root_ca" parameter is now deprecated, you should use the "verify" parameter.

- "to_json" now return a dictionary for keys: "safe", "account" and "api_options".

raise exception changes:
- new exception "CyberarkNotFoundException" will be raise for a http error 404 (page not found) instead of "CyberarkException".

Internal modification for a better management for file configuration and serialization mainly in config.py and cyberark.py.

### Bugfixes
- Fixed a bug where multiple file category where not all updated in some conditions

## [0.1.6] - 2024-03-14
### Bugfixes
- add keep_cookies to serialized aim fields


## [0.1.5] - 2024-03-08
### Bugfixes
- Tests were not all functional
Expand All @@ -34,7 +105,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1.1] - 2024-02-03
### Changes
- Add "get_safe_details" method
- Add support for "custom" configs to override the default logon and reconcile account index
- Add support for "custom" configs to override the default logon and reconcile account index.
**DO NOT USE, deprecated in 0.1.7**.
- Add support to retain cookies during login, and use for subsequent API calls for load-balanced PVWAs.

## [0.1.0] - 2024-01-26
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ If you wish to contribute with code the workflow is :
## Test

- In order to test you need a working Vault and a PVWA.
- Then, generate some accounts with mockaroo and the following schemas : https://www.mockaroo.com/b41fedb0. See "Troubleshoot"
- Then, generate some accounts with mockaroo and the following schemas : https://www.mockaroo.com/1e429890. See "Troubleshoot"
section of some cleanup to avoid issues.
- Create the associated safes : sample-it-dept,sample-iaadmins,sample-coolteam
- Create safe "RENAME_ME", and grant user "admin_bot" (see below) to the "Safe Management" permissions (for safe
Expand All @@ -48,6 +48,9 @@ If you wish to contribute with code the workflow is :
You need an API user, such as **"admin_bot"**, to run the testing. This account is similar to Administrator for
permissions, but you can't use "Administrator" itself (you will get "PASWS291E You cannot perform this
task with an Administrator user. Log on with a different user and try again" error)

The username of the API user you chose must be set in tests/__init__.py (and then just don't commit this file please.)

* In Private Ark Client:
* Add to "Vault Admins" and "PVWAUsers" groups
* Give "Add Safes, Audit Users, Add/Update Users, Reset Users' Passwords, Activate Users" authorizations rules.
Expand Down Expand Up @@ -95,7 +98,6 @@ backend my_backend
logs off.
* `PASWS032E Platform [Oracle] is not active`: The Oracle platform is not activated.


## Update documentation

If your commit has an impact on documentation, please don't forget to update it accordingly.
84 changes: 47 additions & 37 deletions aiobastion/accountgroup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import logging
import re
import warnings
from typing import Union, List

from .accounts import PrivilegedAccount
from .exceptions import AiobastionException, CyberarkAPIException
from aiobastion.exceptions import AiobastionException, CyberarkAPIException, AiobastionConfigurationException, CyberarkNotFoundException


class PrivilegedAccountGroup:
Expand All @@ -14,54 +13,68 @@ def __init__(self, GroupName: str, GroupPlatformID: str, Safe: str, GroupID: str
self.safe = Safe

# ready to add json representation
def to_json(self):
def to_json(self) -> dict:
json_object = {
"GroupName": self.name,
"GroupPlatformID": self.group_platform,
"Safe": self.safe
}
return json_object

def __str__(self):
def __str__(self) -> str:
return f"id : {self.id}, name: {self.name}, group_platform: {self.group_platform}, safe: {self.safe}"



class AccountGroup:
def __init__(self, epv):
_SERIALIZED_FIELDS = []

def __init__(self, epv, **kwargs):
"""
AccountGroup Account group management
"""
self.epv = epv
_section = "accountgroup"
_config_source = self.epv.config.config_source

# Check for unknown attributes
if kwargs:
raise AiobastionConfigurationException(
f"Unknown attribute in section '{_section}' from {_config_source}: {', '.join(kwargs.keys())}"
)

def to_json(self) -> dict:
return {attr_name: getattr(self, attr_name) for attr_name in AccountGroup._SERIALIZED_FIELDS if getattr(self, attr_name, None) is not None}


# Account groups
async def list_by_safe(self, safe_name: str):
async def list_by_safe(self, safe_name: str) -> List[PrivilegedAccountGroup]:
"""
List all groups for a given safe
:param safe_name: name of the safe
:return: a list of PrivilegedAccountGroups
"""
params = {
"Safe": safe_name
}
params = {"Safe": safe_name}
groups = await self.epv.handle_request("get", "api/AccountGroups", params=params)
return [PrivilegedAccountGroup(**g) for g in groups]

async def get_privileged_account_group_id(self, account_group: PrivilegedAccountGroup):
async def get_privileged_account_group_id(self, account_group: PrivilegedAccountGroup) -> str:
"""
Internal function to get the group ID in functions
:param account_group: PrivilegedAccountGroup object
:return: group ID
"""
if account_group.id == "":
if not account_group.id:
acc = await self.list_by_safe(account_group.safe)
for a in acc:
if a.name == account_group.name:
return a.id
raise AiobastionException(f"No ID found for group {account_group.name}")
else:
return account_group.id
return account_group.id

async def get_account_group_id(self, group_name: str, safe: str):
async def get_account_group_id(self, group_name: str, safe: str) -> str:
"""
Get account_group_id with the group_name and the safe
Expand All @@ -73,25 +86,22 @@ async def get_account_group_id(self, group_name: str, safe: str):
for _a in ais:
if _a.name.lower() == group_name.lower():
return _a.id

raise AiobastionException(f"Group {group_name} not found in {safe}")

async def get_group_id(self, account_group):
async def get_group_id(self, account_group) -> str:
"""
Internal function to get group_id from object or from group_id
:param account_group: PrivilegedAccountGroup object or group_id
:return: group_id
"""
if type(account_group) is str:
if re.match(r'\d+_\d+', account_group) is not None:
if isinstance(account_group, str):
if re.match(r'\d+_\d+', account_group):
return account_group
else:
raise AiobastionException("The account_group_id provided is not correct")
raise AiobastionException("The account_group_id provided is not correct")
if isinstance(account_group, PrivilegedAccountGroup):
return await self.get_privileged_account_group_id(account_group)
else:
raise AiobastionException("You must provide a valid PrivilegedAccount to function get_account_id")
raise AiobastionException("You must provide a valid PrivilegedAccount to function get_account_id")

async def members(self, group):
"""
Expand Down Expand Up @@ -132,10 +142,13 @@ async def add_privileged_account_group(self, account_group: PrivilegedAccountGro
"""
if not await self.epv.safe.exists(account_group.safe):
raise AiobastionException(f"Safe {account_group.safe} does not exists")
return await self.epv.handle_request("post", "api/AccountGroups", data=account_group.to_json(),
try:
await self.epv.handle_request("post", "api/AccountGroups", data=account_group.to_json(),
filter_func=lambda x: x['GroupID'])
except CyberarkNotFoundException as err:
raise CyberarkNotFoundException(f"Privileged Account group's platform \"{account_group.group_platform}\" not found")

async def add_member(self, account: (PrivilegedAccount, str), group: (PrivilegedAccountGroup, str)):
async def add_member(self, account: Union[PrivilegedAccount, str], group: Union[PrivilegedAccountGroup, str]):
"""
Add accounts to a group (specified by PrivilegedAccountGroup object or group_id)
Expand All @@ -151,7 +164,7 @@ async def add_member(self, account: (PrivilegedAccount, str), group: (Privileged
}
return await self.epv.handle_request("post", f"api/AccountGroups/{group_id}/Members", data=data)

async def delete_member(self, account: (PrivilegedAccount, str), group: (PrivilegedAccountGroup, str)):
async def delete_member(self, account: Union[PrivilegedAccount, str], group: Union[PrivilegedAccountGroup, str]):
"""
Delete the member of an account group
Expand Down Expand Up @@ -189,9 +202,9 @@ async def move_account_group(self, account_group_name: str, src_safe: str, dst_s
if account_group.name.lower() == account_group_name.lower():

try:
logging.debug(f"Creating {account_group} to {dst_safe}")
self.epv.logger.debug(f"Creating {account_group} to {dst_safe}")
new_group_id = await self.add(account_group.name, account_group.group_platform, dst_safe)
logging.debug(f"Newly created group ID : {new_group_id}")
self.epv.logger.debug(f"Newly created group ID : {new_group_id}")

except CyberarkAPIException as err:
if "EPVPA012E" in err.err_message:
Expand All @@ -200,23 +213,20 @@ async def move_account_group(self, account_group_name: str, src_safe: str, dst_s
self.epv.logger.debug(f"Warning : AG already exists and detected with ID : {new_group_id}")
else:
raise
except Exception as err:
# Unhandled exception
raise

ag_members = await self.epv.accountgroup.members(account_group)

# Moving accounts
try:
moved_accounts = await self.epv.account.move(ag_members, dst_safe)
except CyberarkAPIException as err:
raise
logging.debug("Accounts moved !")

self.epv.logger.debug("Accounts moved !")

for agm in moved_accounts:
try:
await self.add_member(agm, new_group_id)
logging.debug(f"Moved {agm} into {new_group_id}")
self.epv.logger.debug(f"Moved {agm} into {new_group_id}")
except:
# Account are moved with their account group
pass
Expand All @@ -241,9 +251,9 @@ def _case_insensitive_getattr(obj, attr):

account_groups = await self.list_by_safe(src_safe)
for ag in account_groups:
logging.debug(f"Current AG is {ag}")
self.epv.logger.debug(f"Current AG is {ag}")
ag_members = (await self.members(ag))
logging.debug(ag_members)
self.epv.logger.debug(ag_members)
if account_filter is not None:
filtered = False
for a in ag_members:
Expand All @@ -256,7 +266,7 @@ def _case_insensitive_getattr(obj, attr):
raise AiobastionException(f"Your filter doesn't exist on account {a} "
f"(bad file category ? {filter_file_category})")
if filtered:
logging.debug("Account group skipped ....")
self.epv.logger.debug("Account group skipped ....")
continue

try:
Expand Down
Loading

0 comments on commit d384532

Please sign in to comment.