Skip to content

Commit

Permalink
cli-eaa 0.5.7 (#22)
Browse files Browse the repository at this point in the history
* Add support to group association during update operation
* Application module now uses its own logger
* Allow to set EdgeRC file using env variable #21
* eventlog module update:
- uses its own logger
- use {OPEN} API endpoints to fetch user access and admin audit events (experimental)
- default API host for current security events
  • Loading branch information
bitonio committed Apr 13, 2023
1 parent 5934e06 commit d69636b
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 84 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.6
0.5.7
11 changes: 7 additions & 4 deletions bin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,18 @@ def __init__(self, config_values, configuration, flags=None):
parser.add_argument('--batch', '-b', default=False, action='store_true',
help='Batch mode, remove the extra header/footer in lists.')
parser.add_argument('--debug', '-d', default=False, action='count', help=' Debug mode (log HTTP headers)')
parser.add_argument('--edgerc', '-e', default='~/.edgerc', metavar='credentials_file',
help=' Location of the credentials file (default is %s)' % os.path.expanduser("~/.edgerc"))
parser.add_argument('--edgerc', '-e',
default=os.environ.get('AKAMAI_EDGERC', '~/.edgerc'),
metavar='credentials_file',
help=' Credentials file, [$AKAMAI_EDGERC], then %s)' %
os.path.expanduser("~/.edgerc"))
parser.add_argument('--proxy', '-p', default='', help=''' HTTP/S Proxy Host/IP and port number,
do not use prefix (e.g. 10.0.0.1:8888)''')
parser.add_argument('--section', '-c', default=os.environ.get('AKAMAI_EDGERC_SECTION', 'default'),
metavar='credentials_file_section', action='store',
help=' Credentials file Section\'s name to use (\'default\' if not specified).')
help=' Credentials file Section\'s name to use [$AKAMAI_EDGERC_SECTION]')
parser.add_argument('--accountkey', '--account-key', default=os.environ.get('AKAMAI_EDGERC_ACCOUNT_KEY', None),
help=' Account switch key')
help=' Account Switch Key [$AKAMAI_EDGERC_ACCOUNT_KEY]')

parser.add_argument('--verbose', '-v', default=False, action='store_true', help=' Verbose mode')
parser.add_argument('--logfile', default=None, help=' Log file')
Expand Down
2 changes: 1 addition & 1 deletion cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"commands": [
{
"name": "eaa",
"version": "0.5.6",
"version": "0.5.7",
"description": "Akamai CLI for Enterprise Application Access (EAA)"
}
]
Expand Down
133 changes: 93 additions & 40 deletions libeaa/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
# 3rd party
from jinja2 import Environment, FileSystemLoader

logger = logging.getLogger(__name__)


class ApplicationAPI(BaseAPI):
"""
Expand Down Expand Up @@ -87,7 +89,7 @@ def process_command(self):
for line in sys.stdin:
scanned_items = line.split(',')
if len(scanned_items) == 0:
logging.warning("Cannot parse line: %s" % line)
logger.warning("Cannot parse line: %s" % line)
continue
try:
scanned_obj = EAAItem(scanned_items[0])
Expand All @@ -96,11 +98,11 @@ def process_command(self):
elif scanned_obj.objtype == EAAItem.Type.ApplicationGroupAssociation:
appgroups.append(scanned_obj)
except EAAInvalidMoniker:
logging.warning("Invalid application moniker: %s" % scanned_items[0])
logger.warning("Invalid application moniker: %s" % scanned_items[0])
else:
logging.info("Single app %s" % config.application_id)
logger.info("Single app %s" % config.application_id)
applications.append(EAAItem(config.application_id))
logging.info("%s" % EAAItem(config.application_id))
logger.info("%s" % EAAItem(config.application_id))

if config.action == "deploy":
for a in applications:
Expand Down Expand Up @@ -247,17 +249,17 @@ def parse_template(self, raw_config):
"""
Parse the EAA configuration as JINJA2 template
"""
logging.debug("Jinja template loader base directory: %s" % os.getcwd())
logger.debug("Jinja template loader base directory: %s" % os.getcwd())
t = Environment(loader=FileSystemLoader(os.getcwd())).from_string(raw_config)
t.globals['AppProfile'] = ApplicationAPI.Profile
t.globals['AppType'] = ApplicationAPI.Type
t.globals['AppDomainType'] = ApplicationAPI.Domain
t.globals['cli_cloudzone'] = self.cloudzone_lookup
t.globals['cli_certificate'] = self.certificate_lookup
output = t.render()
logging.debug("JSON Post-Template Render:")
logger.debug("JSON Post-Template Render:")
for lineno, line in enumerate(output.splitlines()):
logging.debug("{:4d}> {}".format(lineno+1, line))
logger.debug("{:4d}> {}".format(lineno+1, line))
return output

def create(self, raw_app_config):
Expand All @@ -271,7 +273,7 @@ def create(self, raw_app_config):
We should do the same here
"""
app_config = json.loads(self.parse_template(raw_app_config))
logging.debug("Post Jinja parsing:\n%s" % json.dumps(app_config))
logger.debug("Post Jinja parsing:\n%s" % json.dumps(app_config))
app_config_create = {
"app_profile": app_config.get('app_profile'),
"app_type": app_config.get('app_type', ApplicationAPI.Type.Hosted.value),
Expand All @@ -283,13 +285,13 @@ def create(self, raw_app_config):
app_config_create["client_app_mode"] = \
app_config.get('client_app_mode', ApplicationAPI.ClientMode.TCP.value)
newapp = self.post('mgmt-pop/apps', json=app_config_create)
logging.info("Create app core: %s %s" % (newapp.status_code, newapp.text))
logger.info("Create app core: %s %s" % (newapp.status_code, newapp.text))
if newapp.status_code != 200:
cli.exit(2)
newapp_config = newapp.json()
logging.info("New app JSON:\n%s" % newapp.text)
logger.info("New app JSON:\n%s" % newapp.text)
app_moniker = EAAItem("app://{}".format(newapp_config.get('uuid_url')))
logging.info("UUID of the newapp: %s" % app_moniker)
logger.info("UUID of the newapp: %s" % app_moniker)

# Now we push everything else as a PUT (update)
self.put('mgmt-pop/apps/{applicationId}'.format(applicationId=app_moniker.uuid), json=app_config)
Expand Down Expand Up @@ -322,31 +324,31 @@ def set_acl(self, app_moniker, app_config):
:app_config: details of the configuration to save
"""
# UUID for the ACL service in the newly created application
logging.debug("Fetch service UUID...")
logger.debug("Fetch service UUID...")
services_resp = self.get('mgmt-pop/apps/{app_uuid}/services'.format(app_uuid=app_moniker.uuid))
service_uuid = None
logging.info(json.dumps(services_resp.json(), indent=4))
logger.info(json.dumps(services_resp.json(), indent=4))
for s in services_resp.json().get('objects', []):
scanned_service_type = s.get('service', {}).get('service_type')
logging.debug("Scanned service_type: %s" % scanned_service_type)
logger.debug("Scanned service_type: %s" % scanned_service_type)
if scanned_service_type == ApplicationAPI.ServiceType.ACL.value:
service_uuid = s.get('service', {}).get('uuid_url')
break # Only one service of type ACL
logging.debug("Service UUID for the app is %s" % service_uuid)
logger.debug("Service UUID for the app is %s" % service_uuid)

if service_uuid:

# Obtain ACL service details from the input configuration
service_acl = None
for s in app_config.get('Services', []):
scanned_service_type = s.get('service', {}).get('service_type')
logging.info("Scanned service_type: %s" % scanned_service_type)
logger.info("Scanned service_type: %s" % scanned_service_type)
if scanned_service_type == ApplicationAPI.ServiceType.ACL.value:
service_acl = s
break # Only one service of type ACL

if not service_acl:
logging.warning("No acl rules defined in the application configuration JSON document, skipping")
logger.warning("No acl rules defined in the application configuration JSON document, skipping")
return

# Enable the ACL service
Expand All @@ -371,7 +373,7 @@ def set_acl(self, app_moniker, app_config):

else:

logging.warning("Unable to find a ACL service in the newly created application %s" % app_moniker)
logger.warning("Unable to find a ACL service in the newly created application %s" % app_moniker)

def create_auth(self, app_moniker, app_config):
"""
Expand All @@ -384,27 +386,22 @@ def create_auth(self, app_moniker, app_config):
if scanned_idp_uuid:
idp_app_payload = {"app": app_moniker.uuid, "idp": scanned_idp_uuid}
idp_app_resp = self.post('mgmt-pop/appidp', json=idp_app_payload)
logging.info("IdP-app association response: %s %s" % (idp_app_resp.status_code, idp_app_resp.text))
logger.info("IdP-app association response: %s %s" % (idp_app_resp.status_code, idp_app_resp.text))

# Directory
# The view operation gives us the directories in directories[] -> uuid_url
scanned_directories = app_config.get('directories', [])
app_directories_payload = {"data": [{"apps": [app_moniker.uuid], "directories": scanned_directories}]}
app_directories_resp = self.post('mgmt-pop/appdirectories', json=app_directories_payload)
logging.info(
logger.info(
"App directories association response: %s %s" %
(app_directories_resp.status_code, app_directories_resp.text)
)
if app_directories_resp.status_code != 200:
cli.exit(2)

# Groups
if len(app_config.get('groups', [])) > 0:
app_groups_payload = {'data': [{'apps': [app_moniker.uuid], 'groups': app_config.get('groups', [])}]}
app_groups_resp = self.post('mgmt-pop/appgroups', json=app_groups_payload)
if app_groups_resp.status_code != 200:
cli.exit(2)
else:
logging.debug("No group set")
self.set_appgroups(app_moniker, app_config)

def create_urlbasedpolicies(self, app_moniker, app_config):
if len(app_config.get('urllocation', [])) > 0:
Expand All @@ -425,7 +422,7 @@ def create_urlbasedpolicies(self, app_moniker, app_config):
json=upp_rule
)
else:
logging.debug("No URL path-based policies set")
logger.debug("No URL path-based policies set")

def update(self, app_moniker, app_config):
"""
Expand All @@ -436,13 +433,70 @@ def update(self, app_moniker, app_config):
'mgmt-pop/apps/{applicationId}'.format(applicationId=app_moniker.uuid),
json=app_config
)
logging.info(f"Update core app HTTP/{update.status_code}: {update.text}")
logger.info(f"Update core app HTTP/{update.status_code}: {update.text}")
if update.status_code != 200:
cli.exit(2)

# Update Access Control Rules
self.set_acl(app_moniker, app_config)

# Directory/Group
self.set_appgroups(app_moniker, app_config)

def set_appgroups(self, app_moniker, app_config):
# TODO: use this method in create_auth to set_auth to better fit both create and update
# self.create_auth(app_moniker, app_config)

# Update require to pull the latest list of group (on Akamai Cloud)
# compare with what's in the incoming payload
# new groups added to mgmt-pop/appgroups?
# payload: {'data': [{'apps': [app_moniker.uuid], 'groups': app_config.get('groups', [])}]}
# remove groups that are not in the incoming payload to mgmt-pop/appgroups?method=DELETE
# {"deleted_objects":["ShTjwZjeRg2XjXvv--tHxQ"]}
# where the UUID in the list is the app group UUID
# Example
# Add "support"
# {"data":[{"apps":["PT0JVO4qS-m1g2-ja7-h_Q"],"groups":[{"uuid_url":"9hDCxROqTYmhokljs6uAsA","enable_mfa":"inherit"}]}]}
# Remove "support"
# {"deleted_objects":["S22ijoezTcmJ70l85O423A"]}

if app_config.get('groups'): # if the group key is not in the json, we don't touch anything
existing_groups_resp = self.get(f'mgmt-pop/apps/{app_moniker.uuid}/groups/', params={'limit': 0})
existing_groups = existing_groups_resp.json().get('objects', [])
existing_groups_uuid_map = dict() # mapping between group UUID and app-group UUID association
logger.debug(f"existing_groups_resp:\n{json.dumps(existing_groups, indent=2)}")
existing_group_uuids = set()
for appgroup in existing_groups:
scan_dirguuid = appgroup.get('group', {}).get('group_uuid_url')
scan_apguuid = appgroup.get('uuid_url')
existing_group_uuids.add(scan_dirguuid)
existing_groups_uuid_map[scan_dirguuid] = scan_apguuid

incoming_group_uuids = set()
for appgroup in app_config.get('groups', []):
incoming_group_uuids.add(appgroup.get('uuid_url'))

logger.debug(f"existing_appgroup_uuids={existing_group_uuids}")
logger.debug(f"incoming_appgroup_uuids={incoming_group_uuids}")

# Groups to delete
delete_payload = {"deleted_objects": []}
for group_uuid_to_delete in (existing_group_uuids - incoming_group_uuids):
logger.debug(f"Deleting group UUID {group_uuid_to_delete}, "
f"appgroup UUID {existing_groups_uuid_map[group_uuid_to_delete]}...")
delete_payload["deleted_objects"].append(existing_groups_uuid_map[group_uuid_to_delete])
logger.debug(f"payload: {delete_payload}")
self.post('mgmt-pop/appgroups', params={'method': "DELETE"}, json=delete_payload)

# Group to add/ensure are presents
if len(app_config.get('groups', [])) > 0:
app_groups_payload = {'data': [{'apps': [app_moniker.uuid], 'groups': app_config.get('groups', [])}]}
app_groups_resp = self.post('mgmt-pop/appgroups', json=app_groups_payload)
if app_groups_resp.status_code != 200:
cli.exit(2)
else:
logger.debug("No access groups set.")

def attach_connectors(self, app_moniker, connectors):
"""
Attach connector/s to an application.
Expand All @@ -453,13 +507,13 @@ def attach_connectors(self, app_moniker, connectors):
# POST on mgmt-pop/apps/DBMcU6FwSjKa7c9sny4RLg/agents
# Body
# {"agents":[{"uuid_url":"cht3_GEjQWyMW9LEk7KQfg"}]}
logging.info("Attaching {} connectors...".format(len(connectors)))
logger.info("Attaching {} connectors...".format(len(connectors)))
api_resp = self.post(
'mgmt-pop/apps/{applicationId}/agents'.format(applicationId=app_moniker.uuid),
json={'agents': connectors}
)
logging.info("Attach connector response: %s" % api_resp.status_code)
logging.info("Attach connector app response: %s" % api_resp.text)
logger.info("Attach connector response: %s" % api_resp.status_code)
logger.info("Attach connector app response: %s" % api_resp.text)
if api_resp.status_code not in (200, 201):
cli.print_error("Connector(s) %s were not attached to application %s [HTTP %s]" %
(','.join([c.get('uuid_url') for c in connectors]), app_moniker, api_resp.status_code))
Expand All @@ -472,30 +526,29 @@ def detach_connectors(self, app_moniker, connectors):
Payload is different from attach above:
{"agents":["cht3_GEjQWyMW9LEk7KQfg"]}
"""
logging.info("Detaching {} connectors...".format(len(connectors)))
logger.info("Detaching {} connectors...".format(len(connectors)))
api_resp = self.post(
'mgmt-pop/apps/{applicationId}/agents'.format(applicationId=app_moniker.uuid),
params={'method': 'delete'}, json={'agents': [c.get('uuid_url') for c in connectors]}
)
logging.info("Detach connector response: %s" % api_resp.status_code)
logging.info("Detach connector app response: %s" % api_resp.text)
logger.info("Detach connector response: %s" % api_resp.status_code)
logger.info("Detach connector app response: %s" % api_resp.text)
if api_resp.status_code not in (200, 204):
cli.print_error("Connector(s) %s were not detached from application %s [HTTP %s]" %
(','.join([c.get('uuid_url') for c in connectors]), app_moniker, api_resp.status_code))
cli.print_error("use 'akamai eaa -v ...' for more info")
cli.exit(2)

def add_dnsexception(self, app_moniker):
logging.info("Adding DNS exception: %s" % config.exception_fqdn)
logger.info("Adding DNS exception: %s" % config.exception_fqdn)
appcfg = self.load(app_moniker)
dns_exceptions = set(appcfg.get('advanced_settings', {}).get('domain_exception_list').split(','))
dns_exceptions |= set(config.exception_fqdn)
appcfg["advanced_settings"]["domain_exception_list"] = ','.join(dns_exceptions)
self.save(app_moniker, appcfg)

def del_dnsexception(self, app_moniker):
logging.info("Remove DNS exception: %s" % config.exception_fqdn)
pass
logger.info("Remove DNS exception: %s" % config.exception_fqdn)

def deploy(self, app_moniker, comment=""):
"""
Expand All @@ -509,6 +562,6 @@ def deploy(self, app_moniker, comment=""):
if comment:
payload["deploy_note"] = comment
deploy = self.post('mgmt-pop/apps/{applicationId}/deploy'.format(applicationId=app_moniker.uuid), json=payload)
logging.info("ApplicationAPI: deploy app response: %s" % deploy.status_code)
logger.info("ApplicationAPI: deploy app response: %s" % deploy.status_code)
if deploy.status_code != 200:
logging.error(deploy.text)
logger.error(deploy.text)
9 changes: 5 additions & 4 deletions libeaa/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""

#: cli-eaa version [PEP 8]
__version__ = '0.5.6'
__version__ = '0.5.7'

import sys
from threading import Event
Expand Down Expand Up @@ -196,15 +196,16 @@ def __init__(self, config=None, api=API_Version.Legacy):
edgerc = EdgeRc(config.edgerc)
section = config.section
self.extra_qs = {}
self.api_ver = api

if api == self.API_Version.Legacy: # Prior to {OPEN} API, used for SIEM API only
self._api_ver = api
if self.api_ver == self.API_Version.Legacy: # Prior to {OPEN} API, used for SIEM API only
self._content_type_json = {'content-type': 'application/json'}
self._content_type_form = \
{'content-type': 'application/x-www-form-urlencoded'}
self._headers = None
# self._baseurl = 'https://%s' % edgerc.get(section, 'host')
self._baseurl = 'https://%s/api/v1/' % edgerc.get(section, 'eaa_api_host')
self._baseurl = 'https://%s/api/v1/' % edgerc.get(section, 'eaa_api_host',
fallback="manage.akamai-access.com")
self._session = requests.Session()
self._session.auth = EAALegacyAuth(
edgerc.get(section, 'eaa_api_key'),
Expand Down

0 comments on commit d69636b

Please sign in to comment.