From bdaf3ebc5af427837a7f8c98f9bd9f86f0b6199e Mon Sep 17 00:00:00 2001 From: Antoine Drochon Date: Fri, 4 Mar 2022 15:41:30 -0800 Subject: [PATCH] Bulk invite users in a particular group Bump version to 0.0.9 --- bin/akamai-mfa | 83 +++++++++++++++++++++++++++++++++++++++++++------- cli.json | 2 +- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/bin/akamai-mfa b/bin/akamai-mfa index 7ace7c5..165ffa8 100755 --- a/bin/akamai-mfa +++ b/bin/akamai-mfa @@ -25,7 +25,9 @@ https://learn.akamai.com/en-us/webhelp/enterprise-mfa/akamai-mfa-logs-from-splun """ +from ctypes import ArgumentError import logging +from nis import match import requests import hashlib import hmac @@ -41,7 +43,7 @@ import csv #: cli-mfa version, see also cli.json -__VERSION__ = "0.0.8" +__VERSION__ = "0.0.9" #: Log formatting aligned with other CLIs LOG_FMT = '%(asctime)s [%(levelname)s] %(threadName)s %(message)s' #: Near real-time, 30s ago is the most recent by default @@ -61,13 +63,23 @@ class cli(): """ @staticmethod - def write(s): - print(s) + def print(s): + sys.stdout.write("%s\n" % s) + sys.stdout.flush() + + @staticmethod + def print_error(s): + sys.stderr.write("%s\n" % s) + sys.stderr.flush() @staticmethod def current_command(): return "akamai mfa " + " ".join(sys.argv[1:]) + @staticmethod + def exit(code): + sys.exit(code) + class MFAConfig(): """ Manage CLI MFA input parameters @@ -102,6 +114,18 @@ class MFAConfig(): eventparser.add_argument("--noreceipt", default=False, action="store_true", help="Discard the receipt attribute to save log space") + # User sub parser, will replace the loaduserparser down the road + user_parser = subparsers.add_parser('users', help="User operations (search, import, etc...)") + # userparser.add_argument("action", choices=['search', 'import', 'invite']) + user_action_parser = user_parser.add_subparsers(dest="action", help='User operations (search, import, etc...)') + + # usersearch_parser = user_action_parser.add_parser("search", help="Search users") + # usersearch_parser.add_argument('-g', '--group', dest="group", help='Limit search to users in this group') + # usersearch_parser.add_argument(dest='filter', default="*", help="Pattern to search user, default is *") + invite_parser = user_action_parser.add_parser("invite", help="Send enrollement invite over email") + invite_parser.add_argument('-g', '--group', dest="group", help='Send invite to member of this group') + + # ad-hoc implementation to support MFA customers loaduserparser = subparsers.add_parser('importusers', help="Import users from a CSV file") loaduserparser.add_argument("--file", "-f", help="CSV file used as input") loaduserparser.add_argument("--ignore-header", "-i", dest="ignore_header", default=False, action="store_true", @@ -286,15 +310,28 @@ class IdentityManagementAPI(BaseAPI): def list_groups(self): """ Fetch the list of groups visible in Akamai MFA. - - https://pushzero-staging.akamai.com/api/v1/open_api.html#get-/api/v1/control/groups - You will need to get all of the groups currently in the system in order to manage - the “group name” -> “group id” lookup since all apis related to groups operate on - their ids, not their names. - """ return self.get("/api/v1/control/groups") + def group_id(self, group_name): + """ + Return Group ID for a given group name + If the group name is not found or API return more than one result, return None + + Args: + group_name (string): Group name + + Returns: + string: ID of the group found + """ + group_info = self.get("/api/v1/control/groups", params={'name': group_name}) + matches = group_info.get('result', {}).get('page', []) + match_count = len(matches) + if match_count != 1: + return None + else: + return matches[0].get('id') + def create_group(self, group_name, group_summary=None): """Create a new group in MFA backend.""" payload = { @@ -358,20 +395,37 @@ class IdentityManagementAPI(BaseAPI): count_group_existing += 1 logger.debug(f"Group {g} was already present, not added.") logger.debug("Final groups: %s" % groups_map) - cli.write(f"{count_group_added} group(s) added, {count_group_existing} group(s) were already existing") + cli.print(f"{count_group_added} group(s) added, {count_group_existing} group(s) were already existing") # Second, bulk user insertion new_users_response = self.create_users(new_users) logger.debug("new_users: %s" % new_users_response) count_user_added = len(new_users_response.get('result', {}).get('created', [])) count_user_exist = len(new_users_response.get('result', {}).get('existing', [])) - cli.write(f"{count_user_added} user(s) added, {count_user_exist} user(s) where already existing and left unchanged") + cli.print(f"{count_user_added} user(s) added, {count_user_exist} user(s) where already existing and left unchanged") # Third, associate user to group for new_user in new_users_response.get('result', {}).get('created', []): group_id = reverse_groups_map[user_groupname_map[new_user.get('username')]] self.associate_users_to_group([new_user.get('id')], group_id) + def enroll_users(self, group_name): + """ + Sends off emails to users in a list of groups. + """ + if not isinstance(group_name, str): + raise ArgumentError("groups must be an string") + + group_id = self.group_id(group_name) + if group_id is None: + cli.print_error("Group %s not found." % group_name) + cli.exit(2) + payload = {'exclude_enrolled_users': True} + payload["groups"] = [group_id] + cli.print("Sending enrollment email to ununrolled users in group %s..." % group_name) + response = self.post("/api/v1/control/email/enroll_users", json=payload) + logger.debug(response) + if __name__ == "__main__": @@ -394,6 +448,13 @@ if __name__ == "__main__": sys.exit(0) elif config.command == 'event': EventAPI.pull_events() + elif config.command == 'users': + identity = IdentityManagementAPI(config, config.mfa_api_signing_key, config.mfa_api_integration_id) + if config.action == "invite": + identity.enroll_users(config.group) + else: + cli.write("not supported") + cli.exit(1) elif config.command == 'importusers': logger.debug("starting import...") identity = IdentityManagementAPI(config, config.mfa_api_signing_key, config.mfa_api_integration_id) diff --git a/cli.json b/cli.json index 35b0ef8..0d0a05f 100644 --- a/cli.json +++ b/cli.json @@ -5,7 +5,7 @@ "commands": [ { "name": "mfa", - "version": "0.0.8", + "version": "0.0.9", "description": "Akamai CLI for MFA" } ]