def _get_okta_group_members(api_client: ApiClient, group_id: str) -> List[str]: """ Get group members from Okta server :param api_client: Okta api client :param group_id: group to fetch members from :return: Array or group membership information """ member_list: List[str] = [] next_url = None while True: try: # https://developer.okta.com/docs/reference/api/groups/#list-group-members if next_url: paged_response = api_client.get(next_url) else: params = { 'limit': 1000, } paged_response = api_client.get_path(f'/{group_id}/users', params) except OktaError as okta_error: logger.debug( f"Got error while going through list group member {okta_error}" ) break member_list.append(paged_response.text) if not is_last_page(paged_response): next_url = paged_response.links.get("next").get("url") else: break return member_list
def get_paged_log_events(self, since=None, until=None, q=None, filter=None, limit=None, url=None): """Get a paged list of log events :param since: filters the lower time bound of the log events published property :type since: datetime or None :param until: filters the upper time bound of the log events published property :type until: datetime or None :param filter: filter expression that filters the results :type filter: str or None :param q: filters the log events results by one or more exact keywords :type q: str or None :rtype: list of dictionaries representing log events :param limit: The number of results returned in the response :type limit: int or None :param url: url that returns a list of log events :type url: str :rtype: PagedResults of log events in JSON format """ if url: response = ApiClient.get(self, url) else: params = { 'since': since, 'until': until, 'q': q, 'filter': filter, 'limit': limit, } response = ApiClient.get_path(self, '/', params=params) return PagedResults(response)
def get_paged_events(self, limit=None, start_date=None, after=None, filter_string=None, url=None): """Get a paged list of Events :param limit: maximum number of events to return :type limit: int or None :param filter_string: string to filter events :type filter_string: str or None :param after: event id that filtering will resume after :type after: str or None :param url: url that returns a list of Event :type url: str or None :rtype: PagedResults of Event """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit, 'startDate': start_date, 'after': after, 'filter': filter_string } response = ApiClient.get_path(self, '/', params=params) return PagedResults(response, Event)
def _get_okta_groups(api_client: ApiClient) -> List[str]: """ Get groups from Okta server :param api_client: Okta api client :return: Array of group information """ group_list: List[str] = [] next_url = None # SDK Bug # get_paged_groups returns User object instead of UserGroup while True: # https://developer.okta.com/docs/reference/api/groups/#list-groups if next_url: paged_response = api_client.get(next_url) else: params = { 'limit': 10000, } paged_response = api_client.get_path('/', params) paged_results = PagedResults(paged_response, UserGroup) group_list.extend(paged_results.result) if not is_last_page(paged_response): next_url = paged_response.links.get("next").get("url") else: break return group_list
def get_paged_groups(self, limit=None, filter_string=None, after=None, url=None): """Get a paged list of UserGroups :param limit: maximum number of groups to return :type limit: int or None :param filter_string: Filter expression for groups :type filter_string: str or None :param after: group id that filtering will resume after :type after: str :param url: url that returns a list of UserGroup :type url: str :rtype: PagedResults of UserGroup """ if url: response = ApiClient.get(self, url) else: params = {'limit': limit, 'filter': filter_string, 'after': after} response = ApiClient.get_path(self, '/', params=params) return PagedResults(response, UserGroup)
def get_paged_users(self, limit=None, filter_string=None, search=None, after=None, url=None): """Get a paged list of Users :param limit: maximum number of users to return :type limit: int or None :param filter_string: string to filter users :type filter_string: str or None :param after: user id that filtering will resume after :type after: str :param url: url that returns a list of User :type url: str :rtype: PagedResults of User """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit, 'after': after, 'filter': filter_string, 'search': search } response = ApiClient.get_path(self, '/', params=params) return PagedResults(response, User)
def _get_okta_applications(api_client: ApiClient) -> List[Dict]: """ Get application data from Okta server :param app_client: api client :return: application data """ app_list: List[Dict] = [] next_url = None while True: try: # https://developer.okta.com/docs/reference/api/apps/#list-applications if next_url: paged_response = api_client.get(next_url) else: params = { 'limit': 500, } paged_response = api_client.get_path('/', params) except OktaError as okta_error: logger.debug(f"Got error while listing applications {okta_error}") break app_list.extend(json.loads(paged_response.text)) if not is_last_page(paged_response): next_url = paged_response.links.get("next").get("url") else: break return app_list
def get_paged_users(self, limit=None, filter_string=None, after=None, url=None): """Get a paged list of Users :param limit: maximum number of users to return :type limit: int or None :param filter_string: string to filter users :type filter_string: str or None :param after: user id that filtering will resume after :type after: str :param url: url that returns a list of User :type url: str :rtype: PagedResults of User """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit, 'after': after, 'filter': filter_string } response = ApiClient.get_path(self, '/', params=params) return PagedResults(response, User)
def get_okta_group_members(api_client: ApiClient, group_id: str) -> List[Dict]: """ Get group members from Okta server :param api_client: Okta api client :param group_id: group to fetch members from :return: Array or group membership information """ member_list: List[Dict] = [] next_url = None while True: try: # https://developer.okta.com/docs/reference/api/groups/#list-group-members if next_url: paged_response = api_client.get(next_url) else: params = { 'limit': 1000, } paged_response = api_client.get_path(f'/{group_id}/users', params) except OktaError: logger.error( f"OktaError while listing members of group {group_id}") raise member_list.extend(json.loads(paged_response.text)) if not is_last_page(paged_response): next_url = paged_response.links.get("next").get("url") else: break return member_list
def get_paged_app_instances(self, limit=None, filter_string=None, after=None, url=None): """Get a paged list of AppInstances :param limit: maximum number of apps to return :type limit: int or None :param filter_string: string to filter apps :type filter_string: str or None :param after: app id that filtering will resume after :type after: str :param url: url that returns a list of AppInstance :type url: str :rtype: PagedResults of AppInstance """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit, 'after': after, 'filter': filter_string } response = ApiClient.get_path(self, '/', params=params) return PagedResults(response, AppInstance)
def _get_aws_account_info(okta_org_url, okta_api_key, username): """ Call the Okta User API and process the results to return just the information we need for gimme_aws_creds""" # We need access to the entire JSON response from the Okta APIs, so we need to # use the low-level ApiClient instead of UsersClient and AppInstanceClient users_client = ApiClient(okta_org_url, okta_api_key, pathname='/api/v1/users') # Get User information try: result = users_client.get_path('/{0}'.format(username)) user = result.json() except OktaError as e: if e.error_code == 'E0000007': print("Error: " + username + " was not found!", file=sys.stderr) exit(1) else: print("Error: " + e.error_summary, file=sys.stderr) exit(1) try: # Get first page of results result = users_client.get_path('/{0}/appLinks'.format(user['id'])) final_result = result.json() # Loop through other pages while 'next' in result.links: result = users_client.get(result.links['next']['url']) final_result = final_result + result.json() print("done\n", file=sys.stderr) except OktaError as e: if e.error_code == 'E0000007': print("Error: No applications found for " + username, file=sys.stderr) exit(1) else: print("Error: " + e.error_summary, file=sys.stderr) exit(1) # Loop through the list of apps and filter it down to just the info we need app_list = [] for app in final_result: # All AWS connections have the same app name if (app['appName'] == 'amazon_aws'): newAppEntry = {} newAppEntry['id'] = app['id'] newAppEntry['name'] = app['label'] newAppEntry['links'] = {} newAppEntry['links']['appLink'] = app['linkUrl'] newAppEntry['links']['appLogo'] = app['logoUrl'] app_list.append(newAppEntry) # Throw an error if we didn't get any accounts back if not app_list: print("No AWS accounts found.", file=sys.stderr) exit() return app_list
def push_verification_poll(self, url): """Poll for push verification :param url: push polling URL :type url: str :rtype: ActivationResponse """ response = ApiClient.get(self, url) return Utils.deserialize(response.text, FactorVerificationResponse)
def get_paged_groups(self, limit=None, after=None, url=None): """Get a paged list of UserGroups :param limit: maximum number of groups to return :type limit: int or None :param after: group id that filtering will resume after :type after: str :param url: url that returns a list of UserGroup :type url: str :rtype: PagedResults of UserGroup """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit, 'after': after } response = ApiClient.get_path(self, '/', params=params) return PagedResults(response, User)
def get_paged_group_members(self, gid, url=None, limit=None, after=None): """Get a paged list of users from a group :param gid: the group id :type gid: str :param limit: maximum number of users to return :type limit: int or None :param after: user id that filtering will resume after :type after: str :param url: url that returns a list of User :type url: str :rtype: PagedResults of User """ if url: response = ApiClient.get(self, url) else: params = {'limit': limit, 'after': after} response = ApiClient.get_path(self, '/{0}/users'.format(gid), params=params) return PagedResults(response, User)
def get_app_paged_users(self, gid=None, limit=None, after=None, url=None): """Get a paged list of AppUsers of an app :param gid: the app id :type gid: str :param limit: maximum number of AppUser to return :type limit: int or None :param after: app id that filtering will resume after :type after: str :param url: url that returns a list of AppUser :type url: str :rtype: AppUser """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit, 'after': after } response = ApiClient.get_path(self, '/{0}/users'.format(gid), params=params) return PagedResults(response, AppUser)
def _get_application_assigned_users(api_client: ApiClient, app_id: str) -> List[str]: """ Get users assigned to a specific application :param api_client: api client :param app_id: application id to get users from :return: Array of user data """ app_users: List[str] = [] next_url = None while True: try: # https://developer.okta.com/docs/reference/api/apps/#list-users-assigned-to-application if next_url: paged_response = api_client.get(next_url) else: params = { 'limit': 500, } paged_response = api_client.get_path(f'/{app_id}/users', params) except OktaError as okta_error: logger.debug( f"Got error while going through list application assigned users {okta_error}" ) break app_users.append(paged_response.text) if not is_last_page(paged_response): next_url = paged_response.links.get("next").get("url") else: break return app_users
def get_paged_app_targets_for_app_admin(self, uid, rid, url=None, limit=None): """Get a paged list of app targets for an APP_ADMIN role assignment :param uid: the user id :type uid: str :param rid: the APP_ADMIN role id :type rid: str :param url: url that returns a list of app targets :type url: str :param limit: maximum number of Apps to return :type limit: int or None :rtype: Array of CatalogApp """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit } url_path = '/{0}/roles/{1}/targets/catalog/apps'.format(uid, rid) response = ApiClient.get_path(self, url_path, params=params) # TODO: create Catalog App Model return PagedResults(response, AppInstance)
def _get_application_assigned_groups(api_client: ApiClient, app_id: str) -> List[str]: """ Get groups assigned to a specific application :param api_client: api client :param app_id: application id to get users from :return: Array of group id """ app_groups: List[str] = [] next_url = None while True: try: if next_url: paged_response = api_client.get(next_url) else: params = { 'limit': 500, } paged_response = api_client.get_path(f'/{app_id}/groups', params) except OktaError as okta_error: logger.debug( f"Got error while going through list application assigned groups {okta_error}" ) break app_groups.append(paged_response.text) if not is_last_page(paged_response): next_url = paged_response.links.get("next").get("url") else: break return app_groups
def get_paged_group_targets_for_user_admin(self, uid, rid, url=None, limit=None): """Get a paged list of group targets for an USER_ADMIN role assignment :param uid: the user id :type uid: str :param rid: the USER_ADMIN role id :type rid: str :param url: url that returns a list of group targets :type url: str :param limit: maximum number of Group to return :type limit: int or None :rtype: Array of UserGroup """ if url: response = ApiClient.get(self, url) else: params = { 'limit': limit } url_path = '/{0}/roles/{1}/targets/groups'.format(uid, rid) response = ApiClient.get_path(self, url_path, params=params) return PagedResults(response, UserGroup)
def aws_account_info(event, context): # We need access to the entire JSON response from the Okta APIs, so we need to # use the low-level ApiClient instead of UsersClient and AppInstanceClient usersClient = ApiClient(os.environ['OKTA_ORG_URL'], os.environ['OKTA_API_KEY'], pathname='/api/v1/users') appClient = ApiClient(os.environ['OKTA_ORG_URL'], os.environ['OKTA_API_KEY'], pathname='/api/v1/apps') # Get User information username = event['requestContext']['authorizer']['principalId'] try: result = usersClient.get_path('/{0}'.format(username)) user = result.json() except OktaError as e: if e.error_code == 'E0000007': statusCode = 404 else: statusCode = 500 return { 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': True }, "statusCode": statusCode, "body": e.error_summary } # Get a list of apps for this user and include extended info about the user params = { 'limit': 200, 'filter': 'user.id+eq+%22' + user['id'] + '%22&expand=user%2F' + user['id'] } try: # Get first page of results result = usersClient.get_path('/{0}/appLinks'.format(user['id'])) final_result = result.json() # Loop through other pages while 'next' in result.links: result = appClient.get(result.links['next']['url']) final_result = final_result + result.json() except OktaError as e: if e.error_code == 'E0000007': statusCode = 404 else: statusCode = 500 return { 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': True }, "statusCode": statusCode, "body": e.error_summary } # Loop through the list of apps and filter it down to just the info we need appList = [] for app in final_result: # All AWS connections have the same app name if (app['appName'] == 'amazon_aws'): newAppEntry = {} newAppEntry['id'] = app['id'] newAppEntry['name'] = app['label'] newAppEntry['links'] = {} newAppEntry['links']['appLink'] = app['linkUrl'] newAppEntry['links']['appLogo'] = app['logoUrl'] appList.append(newAppEntry) response = { 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': True }, "statusCode": 200, "body": json.dumps(appList) } return response
class AuthClient(BaseAuthClient): def __init__(self, connection_config, reauth_time, auth_attrib) -> None: ''' Args: connection_config (Dict): Parameters required to connect to the Okta API reauth_time (int): The min time in seconds to cache auth requests auth_attrib (str): The attribute of the user record that will be used to authenticate them. ''' super().__init__(reauth_time, auth_attrib) connection_config['pathname'] = '/api/v1/users' self.usersclient = UsersClient(**connection_config) self.factorsclient = FactorsClient(**connection_config) self.apiclient = ApiClient(**connection_config) # Maintain a per user lookup for poll URL self.poll_url = {} def _get_okta_userid(self, username): user = self.usersclient.get_users(query=username, limit=1) try: return user[0].id except Exception as error: logging.error('Error getting username {}'.format(error)) return None def _get_factors(self, userid): try: return self.factorsclient.get_lifecycle_factors(userid) except Exception as error: logging.error('Error getting factors {}'.format(error)) return None def can_auth(self, user): # type: () -> bool # Check Okta user for a push factor. # Returns false is not available # Returns factor Id if it is # TODO: Add support for other types of auth (TOTP, etc). username = self._auth_attribute(user) if username is not False: logging.debug('Checking auth capabilities for {}'.format(username)) okta_user_id = self._get_okta_userid(username) factors = self._get_factors(okta_user_id) if factors is not None: for factor in factors: if factor.factorType == 'push': return factor.id return False def auth(self, user, reason=None): # type: (str) -> None logging.debug('Sending Okta Push request for {}'.format( self._auth_attribute(user))) # Okta's SDK is broken! # https://github.com/okta/okta-sdk-python/issues/66 # res = self.factorsclient.verify_factor( # user_id=self.okta_user_id, # user_factor_id=self.okta_push_factor_id # ) # Implement our own call which actually works okta_user_id = self._get_okta_userid(self._auth_attribute(user)) res = self.apiclient.post_path('/{0}/factors/{1}/verify'.format( okta_user_id, user._factor_id)) try: res_obj = json.loads(res.text) except Exception as error: raise AuthException(error) self.poll_url[okta_user_id] = res_obj['_links']['poll']['href'] user._last_auth_state = AuthStates.PENDING def auth_status(self, user): # type: () -> int okta_user_id = self._get_okta_userid(self._auth_attribute(user)) if user._last_auth_state == AuthStates.PENDING: response = self.apiclient.get(self.poll_url[okta_user_id]) response_obj = json.loads(response.text) res = response_obj['factorResult'] if res != 'WAITING': if res == 'SUCCESS': user._last_auth_state = AuthStates.AUTHORIZED user._last_auth_time = datetime.now(tz=pytz.utc) else: user._last_auth_state = AuthStates.DENIED user._last_auth_time = datetime.min elif user._last_auth_state == AuthStates.AUTHORIZED: if not self._recently_authed(user): user._last_auth_state = AuthStates.NONE return user._last_auth_state def reset(self, user): okta_user_id = self._get_okta_userid(self._auth_attribute(user)) self.poll_url.pop(okta_user_id, None) user._last_auth_state = AuthStates.NONE
def _get_aws_account_info(okta_org_url, okta_api_key, username): """ Call the Okta User and App APIs and process the results to return just the information we need for gimme_aws_creds""" # We need access to the entire JSON response from the Okta APIs, so we need to # use the low-level ApiClient instead of UsersClient and AppInstanceClient users_client = ApiClient(okta_org_url, okta_api_key, pathname='/api/v1/users') app_client = ApiClient(okta_org_url, okta_api_key, pathname='/api/v1/apps') # Get User information try: result = users_client.get_path('/{0}'.format(username)) user = result.json() except OktaError as e: if e.error_code == 'E0000007': print("Error: " + username + " was not found!") exit(1) else: print("Error: " + e.error_summary) exit(1) # Get a list of apps for this user and include extended info about the user params = { 'limit': 50, 'filter': 'user.id+eq+%22' + user['id'] + '%22&expand=user%2F' + user['id'] } try: # Get first page of results result = app_client.get_path('/', params=params) final_result = result.json() # Loop through other pages while 'next' in result.links: print('.', end='', flush=True) result = app_client.get(result.links['next']['url']) final_result = final_result + result.json() print("done\n") except OktaError as e: if e.error_code == 'E0000007': print("Error: No applications found for " + username) exit(1) else: print("Error: " + e.error_summary) exit(1) # Loop through the list of apps and filter it down to just the info we need app_list = [] for app in final_result: # All AWS connections have the same app name if app['name'] == 'amazon_aws': new_app_entry = { 'id': app['id'], 'name': app['label'], 'identityProviderArn': app['settings']['app']['identityProviderArn'], 'roles': [] } # Build a list of the roles this user has access to for role in app['_embedded']['user']['profile']['samlRoles']: role_info = { 'name': role, 'arn': re.sub(':saml-provider.*', ':role/' + role, app['settings']['app']['identityProviderArn']) } # We can figure out the role ARN based on the ARN for the IdP new_app_entry['roles'].append(role_info) new_app_entry['links'] = {} new_app_entry['links']['appLink'] = app['_links']['appLinks'][ 0]['href'] new_app_entry['links']['appLogo'] = app['_links']['logo'][0][ 'href'] app_list.append(new_app_entry) # Throw an error if we didn't get any accounts back if not app_list: print("No AWS accounts found.") exit() return app_list