Пример #1
0
    def test_login_post_invalid_password(self):
        from consoleme.lib.dynamo import UserDynamoHandler

        ddb = UserDynamoHandler()
        ddb.create_user("testuser", "correctpassword",
                        ["group1", "*****@*****.**"])
        body = json.dumps({
            "username": "******",
            "password": "******",
            "after_redirect_uri": "/"
        })
        response = self.fetch("/api/v2/login", method="POST", body=body)
        self.assertEqual(response.code, 403)
        self.assertEqual(
            json.loads(response.body),
            {
                "status":
                "error",
                "reason":
                "authentication_failure",
                "redirect_url":
                None,
                "status_code":
                403,
                "message":
                None,
                "errors": [
                    "User doesn't exist, or password is incorrect. ",
                    "Your next authentication failure will result in a 1 second wait. This wait time will expire after 60 seconds of no authentication failures.",
                ],
                "data":
                None,
            },
        )
Пример #2
0
async def api_add_user_to_group_or_raise(group_name, member_name, actor):
    try:
        group_info = await auth.get_group_info(group_name, members=False)
    except Exception:
        raise NoGroupsException("Unable to retrieve the specified group")

    actor_groups = await auth.get_groups(actor)
    can_add_remove_members = can_modify_members(actor, actor_groups, group_info)

    if not can_add_remove_members:
        raise UnauthorizedToAccess("Unauthorized to modify members of this group.")

    try:
        await add_user_to_group(member_name, group_name, actor)
    except HttpError as e:
        # Inconsistent GG API error - ignore failure for user already existing
        if e.resp.reason == "duplicate":
            pass
    except UserAlreadyAMemberOfGroupException:
        pass
    except BulkAddPrevented:
        dynamo_handler = UserDynamoHandler(actor)
        dynamo_handler.add_request(
            member_name,
            group_name,
            f"{actor} requesting on behalf of {member_name} from a bulk operation",
            updated_by=actor,
        )
        return "REQUESTED"

    return "ADDED"
Пример #3
0
    def test_login_post_success(self):
        from consoleme.lib.dynamo import UserDynamoHandler

        ddb = UserDynamoHandler()
        ddb.create_user("testuser2", "correctpassword",
                        ["group1", "*****@*****.**"])
        body = json.dumps({
            "username": "******",
            "password": "******",
            "after_redirect_uri": "/",
        })
        response = self.fetch("/api/v2/login", method="POST", body=body)
        self.assertEqual(response.code, 200)
        self.assertEqual(
            json.loads(response.body),
            {
                "status": "redirect",
                "reason": "authenticated_redirect",
                "redirect_url": "/",
                "status_code": 200,
                "message":
                "User has successfully authenticated. Redirecting to their intended destination.",
                "errors": None,
                "data": None,
            },
        )
Пример #4
0
async def write_notification(notification: ConsoleMeUserNotification):
    ddb = UserDynamoHandler()
    await sync_to_async(ddb.notifications_table.put_item
                        )(Item=ddb._data_to_dynamo_replace(notification.dict())
                          )
    await cache_notifications_to_redis_s3()
    return True
Пример #5
0
def refresh_dynamic_config(ddb=None):
    if not ddb:
        # This function runs frequently. We provide the option to pass in a UserDynamoHandler
        # so we don't need to import on every invocation
        from consoleme.lib.dynamo import UserDynamoHandler

        ddb = UserDynamoHandler()
    return ddb.get_dynamic_config_dict()
Пример #6
0
    def load_config_from_dynamo(self):
        """If enabled, we can load a configuration dynamically from Dynamo at a certain time interval. This reduces
        the need for code redeploys to make configuration changes"""
        from consoleme.lib.dynamo import UserDynamoHandler
        from consoleme.lib.redis import RedisHandler

        ddb = UserDynamoHandler()
        red = RedisHandler().redis_sync()

        while True:
            dynamic_config = refresh_dynamic_config(ddb)
            if dynamic_config and dynamic_config != self.config.get("dynamic_config"):
                red.set(
                    "DYNAMIC_CONFIG_CACHE",
                    json.dumps(dynamic_config),
                )
                self.get_logger("config").debug(
                    {
                        "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
                        "message": "Dynamic configuration changes detected and loaded",
                        "dynamic_config": dynamic_config,
                    }
                )
                self.config["dynamic_config"] = dynamic_config
            time.sleep(self.get("dynamic_config.dynamo_load_interval", 60))
Пример #7
0
async def get_app_pending_requests_policies(user):
    dynamo_handler = UserDynamoHandler(user)
    all_policy_requests = await dynamo_handler.get_all_policy_requests(
        status="pending")
    if not all_policy_requests:
        all_policy_requests = []
    return all_policy_requests
Пример #8
0
async def get_all_policy_requests(user, status=None):
    dynamo_handler = UserDynamoHandler(user)
    all_policy_requests = await dynamo_handler.get_all_policy_requests(
        status=status)
    if not all_policy_requests:
        all_policy_requests = []
    return all_policy_requests
Пример #9
0
async def get_all_pending_requests_api(user):
    """Get all pending requests and add the group's secondary approvers"""
    dynamo_handler = UserDynamoHandler(user)
    all_requests = await dynamo_handler.get_all_requests()

    pending_requests = []

    # Get secondary approvers for groups asynchronously, otherwise this can be a bottleneck
    tasks = []
    for req in all_requests:
        if req.get("status") == "pending":
            group = req.get("group")
            task = asyncio.ensure_future(
                auth.get_secondary_approvers(group, return_dict=True)
            )
            tasks.append(task)
            pending_requests.append(req)
    secondary_approver_responses = asyncio.gather(*tasks)
    secondary_approver_mapping = {}
    for mapping in await secondary_approver_responses:
        for group, secondary_approvers in mapping.items():
            secondary_approver_mapping[group] = ",".join(secondary_approvers)

    for req in pending_requests:
        req["secondary_approvers"] = secondary_approver_mapping.get(
            req.get("group"), ""
        )
    return pending_requests
Пример #10
0
    async def _get_extended_request(self, request_id, log_data):
        dynamo = UserDynamoHandler(self.user)
        requests = await dynamo.get_policy_requests(request_id=request_id)
        if len(requests) == 0:
            log_data["message"] = "Request with that ID not found"
            log.warn(log_data)
            stats.count(f"{log_data['function']}.not_found",
                        tags={"user": self.user})
            raise NoMatchingRequest(log_data["message"])
        if len(requests) > 1:
            log_data["message"] = "Multiple requests with that ID found"
            log.error(log_data)
            stats.count(
                f"{log_data['function']}.multiple_requests_found",
                tags={"user": self.user},
            )
            raise InvalidRequestParameter(log_data["message"])
        request = requests[0]

        if request.get("version") != "2":
            # Request format is not compatible with this endpoint version
            raise InvalidRequestParameter(
                "Request with that ID is not a v2 request")

        extended_request = ExtendedRequestModel.parse_obj(
            request.get("extended_request"))
        return extended_request, request.get("last_updated")
Пример #11
0
async def fetch_notification(notification_id: str):
    ddb = UserDynamoHandler()
    notification = await sync_to_async(ddb.notifications_table.get_item
                                       )(Key={
                                           "predictable_id": notification_id
                                       })
    if notification.get("Item"):
        return ConsoleMeUserNotification.parse_obj(notification["Item"])
Пример #12
0
async def add_user_to_group(
    user_email: str,
    google_group_email: str,
    updated_by: Optional[str] = None,
    role: str = "MEMBER",
    dry_run: None = None,
    service: Optional[Resource] = None,
    request: Optional[Dict[str, Union[int, str]]] = None,
) -> Dict[str, bool]:
    """Add user member to group."""
    dynamo = UserDynamoHandler()
    result = {"done": True}
    function = f"{__name__}.{sys._getframe().f_code.co_name}"
    stats.count(function)
    log_data = {
        "function": function,
        "user_email": user_email,
        "google_group_email": google_group_email,
        "updated_by": updated_by,
        "role": role,
        "dry_run": dry_run,
        "message": "Adding user to group",
    }
    if not service:
        service = await get_service("admin", "directory_v1",
                                    google_group_email)
    existing = await list_group_members(google_group_email, dry_run=dry_run)

    if user_email in existing:
        log_data[
            "message"] = "Unable to add user to group. User is already a member."
        log.warn(log_data)
        result["done"] = False
        result["message"] = log_data["message"]
        raise UserAlreadyAMemberOfGroupException(result["message"])

    group_info = await auth.get_group_info(google_group_email, members=False)
    await raise_if_requires_bgcheck_and_no_bgcheck(user_email, group_info)
    await raise_if_not_same_domain(user_email, group_info)
    await raise_if_restricted(user_email, group_info)
    await raise_if_bulk_add_disabled_and_no_request(group_info, request)

    if not dry_run:
        stats.count(
            "google.add_user_to_group",
            tags={
                "user_email": user_email,
                "google_group_email": google_group_email,
                "updated_by": updated_by,
            },
        )

        await insert_group_members_call(service, google_group_email,
                                        user_email, role)
        await dynamo.create_group_log_entry(google_group_email, user_email,
                                            updated_by, "Added")
        log.info(log_data)
    return result
Пример #13
0
async def cache_notifications_to_redis_s3() -> Dict[str, int]:
    function = f"{__name__}.{sys._getframe().f_code.co_name}"
    current_time = int(time.time())
    log_data = {"function": function}
    ddb = UserDynamoHandler()
    notifications_by_user_group = defaultdict(list)
    all_notifications_l = await ddb.parallel_scan_table_async(
        ddb.notifications_table)
    changed_notifications = []
    for existing_notification in all_notifications_l:
        notification = ConsoleMeUserNotification.parse_obj(
            existing_notification)
        if current_time > notification.expiration:
            notification.expired = True
            changed_notifications.append(notification.dict())
        for user_or_group in notification.users_or_groups:
            notifications_by_user_group[user_or_group].append(
                notification.dict())

    if changed_notifications:
        ddb.parallel_write_table(ddb.notifications_table,
                                 changed_notifications)

    if notifications_by_user_group:
        for k, v in notifications_by_user_group.items():
            notifications_by_user_group[k] = original_json.dumps(
                v, cls=SetEncoder)
        await store_json_results_in_redis_and_s3(
            notifications_by_user_group,
            redis_key=config.get("notifications.redis_key",
                                 "ALL_NOTIFICATIONS"),
            redis_data_type="hash",
            s3_bucket=config.get("notifications.s3.bucket"),
            s3_key=config.get("notifications.s3.key",
                              "notifications/all_notifications_v1.json.gz"),
        )
    log_data["num_user_groups_for_notifications"] = len(
        notifications_by_user_group.keys())
    log_data["num_notifications"] = len(all_notifications_l)
    log.debug(log_data)
    return {
        "num_user_groups_to_notify": len(notifications_by_user_group.keys()),
        "num_notifications": len(all_notifications_l),
    }
Пример #14
0
async def get_existing_pending_request(user: str, group_info: Any) -> None:
    dynamo_handler = UserDynamoHandler(user)
    existing_requests = await sync_to_async(dynamo_handler.get_requests_by_user)(user)
    if existing_requests:
        for request in existing_requests:
            if group_info.get("name") == request.get("group") and request.get(
                "status"
            ) in ["pending"]:
                return request
    return None
Пример #15
0
async def migrate():
    # Get all policy requests
    # iterate through changes
    # if has principal_arn, convert
    dynamo = UserDynamoHandler("consoleme")
    requests = await dynamo.get_all_policy_requests(status=None)
    for request in requests:
        changes = (request.get("extended_request",
                               {}).get("changes", {}).get("changes", []))
        for change in changes:
            if not change.get("principal_arn"):
                continue
            if not change.get("version") == "2.0":
                continue
            change["principal"] = {
                "principal_arn": change["principal_arn"],
                "principal_type": "AwsResource",
            }
            change.pop("principal_arn")
            change["version"] = "3.0"
    dynamo.parallel_write_table(dynamo.policy_requests_table, requests)
Пример #16
0
async def get_request_by_id(user, request_id):
    """Get request matching id and add the group's secondary approvers"""
    dynamo_handler = UserDynamoHandler(user)
    try:
        requests = await sync_to_async(dynamo_handler.resolve_request_ids)([request_id])
        for req in requests:
            group = req.get("group")
            secondary_approvers = await auth.get_secondary_approvers(group)
            req["secondary_approvers"] = ",".join(secondary_approvers)
    except NoMatchingRequest:
        requests = []
    return next(iter(requests), None)
Пример #17
0
async def remove_user_from_group(
    user_email: str,
    google_group_email: str,
    updated_by: Optional[str] = None,
    dry_run: None = None,
    service: Optional[Resource] = None,
) -> Dict[str, bool]:
    """Remove user member to group."""
    result = {"done": True}
    dynamo = UserDynamoHandler()
    function = f"{__name__}.{sys._getframe().f_code.co_name}"
    stats.count(function)
    log_data = {
        "function": function,
        "user_email": user_email,
        "group": google_group_email,
        "updated_by": updated_by,
        "dry_run": dry_run,
        "message": "Removing user from group",
    }
    log.debug(log_data)

    group_info = await auth.get_group_info(google_group_email, members=False)
    await raise_if_restricted(user_email, group_info)
    if not service:
        service = await get_service("admin", "directory_v1", google_group_email)
    existing = await list_group_members(google_group_email, dry_run=dry_run)

    if user_email in existing:
        if not dry_run:
            stats.count(
                f"{function}.remove_user_from_group",
                tags={
                    "user_email": user_email,
                    "google_group_email": google_group_email,
                    "updated_by": updated_by,
                },
            )
            await delete_group_members_call(service, google_group_email, user_email)
            await dynamo.create_group_log_entry(
                google_group_email, user_email, updated_by, "Removed"
            )
    else:
        log_data[
            "message"
        ] = "Unable to remove user from group. User is not currently in the group."
        log.warn(log_data)
        result["done"] = False
        result["message"] = log_data["message"]
        raise NotAMemberException(result["message"])
    return result
Пример #18
0
    def load_config_from_dynamo_bg_thread(self):
        """If enabled, we can load a configuration dynamically from Dynamo at a certain time interval. This reduces
        the need for code redeploys to make configuration changes"""
        from consoleme.lib.dynamo import UserDynamoHandler
        from consoleme.lib.redis import RedisHandler

        ddb = UserDynamoHandler()
        red = RedisHandler().redis_sync()

        while threading.main_thread().is_alive():
            self.load_config_from_dynamo(ddb, red)
            # Wait till main exit flag is set OR a fixed timeout
            if main_exit_flag.wait(timeout=self.get(
                    "dynamic_config.dynamo_load_interval", 60)):
                break
Пример #19
0
class Policies:
    """
    Policies internal plugin
    """
    def __init__(self, ) -> None:
        self.dynamo = UserDynamoHandler()

    def error_count_by_arn(self):
        try:
            return self.dynamo.count_cloudtrail_errors_by_arn()
        except Exception:
            sentry_sdk.capture_exception()
            return {}

    async def get_errors_by_role(self, arn, n=5):
        try:
            return await self.dynamo.get_top_cloudtrail_errors_by_arn(arn, n)
        except Exception:
            sentry_sdk.capture_exception()
            return {}

    async def get_applications_associated_with_role(
            self, arn: str) -> AppDetailsArray:
        """
        This function returns applications associated with a role from configuration. You may want to override this
        function to pull this information from an authoratative source.

        :param arn: Role ARN
        :return: AppDetailsArray
        """

        apps_formatted = []

        application_details = config.get("application_details", {})

        for app, details in application_details.items():
            apps_formatted.append(
                AppDetailsModel(
                    name=app,
                    owner=details.get("owner"),
                    owner_url=details.get("owner_url"),
                    app_url=details.get("app_url"),
                ))
        return AppDetailsArray(app_details=apps_formatted)
Пример #20
0
async def get_user_requests(user, groups):
    """Get requests relevant to a user.

    A user sees requests they have made as well as requests where they are a
    secondary approver
    """
    dynamo_handler = UserDynamoHandler(user)
    all_requests = await dynamo_handler.get_all_requests()
    query = {
        "domains":
        config.get("dynamo.get_user_requests.domains", []),
        "filters": [
            {
                "field": "extendedattributes.attributeName",
                "values": ["secondary_approvers"],
                "operator": "EQUALS",
            },
            {
                "field": "extendedattributes.attributeValue",
                "values": groups + [user],
                "operator": "EQUALS",
            },
        ],
        "size":
        500,
    }
    approver_groups = await auth.query_cached_groups(query=query)
    approver_groups = [g["name"] for g in approver_groups]

    requests = []
    for req in all_requests:

        if user == req.get("username", ""):
            requests.append(req)
            continue

        group = req.get("group")
        if group is None:
            continue
        if group in approver_groups + [user]:
            requests.append(req)

    return requests
Пример #21
0
    def load_config_from_dynamo(self, ddb=None, red=None):
        if not ddb:
            from consoleme.lib.dynamo import UserDynamoHandler

            ddb = UserDynamoHandler()
        if not red:
            from consoleme.lib.redis import RedisHandler

            red = RedisHandler().redis_sync()

        dynamic_config = refresh_dynamic_config(ddb)
        if dynamic_config and dynamic_config != self.config.get("dynamic_config"):
            red.set(
                "DYNAMIC_CONFIG_CACHE",
                json.dumps(dynamic_config),
            )
            self.get_logger("config").debug(
                {
                    "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
                    "message": "Dynamic configuration changes detected and loaded",
                }
            )
            self.config["dynamic_config"] = dynamic_config
Пример #22
0
async def detect_cloudtrail_denies_and_update_cache(
    celery_app,
    event_ttl=config.get(
        "event_bridge.detect_cloudtrail_denies_and_update_cache.event_ttl",
        86400),
    max_num_messages_to_process=config.get(
        "event_bridge.detect_cloudtrail_denies_and_update_cache.max_num_messages_to_process",
        100,
    ),
) -> Dict[str, Any]:
    log_data = {"function": f"{__name__}.{sys._getframe().f_code.co_name}"}
    dynamo = UserDynamoHandler()
    queue_arn = config.get(
        "event_bridge.detect_cloudtrail_denies_and_update_cache.queue_arn",
        "").format(region=config.region)
    if not queue_arn:
        raise MissingConfigurationValue(
            "Unable to find required configuration value: "
            "`event_bridge.detect_cloudtrail_denies_and_update_cache.queue_arn`"
        )
    queue_name = queue_arn.split(":")[-1]
    queue_account_number = queue_arn.split(":")[4]
    queue_region = queue_arn.split(":")[3]
    # Optionally assume a role before receiving messages from the queue
    queue_assume_role = config.get(
        "event_bridge.detect_cloudtrail_denies_and_update_cache.assume_role")

    # Modify existing cloudtrail deny samples
    all_cloudtrail_denies_l = await dynamo.parallel_scan_table_async(
        dynamo.cloudtrail_table)
    all_cloudtrail_denies = {}
    for cloudtrail_deny in all_cloudtrail_denies_l:
        all_cloudtrail_denies[cloudtrail_deny["request_id"]] = cloudtrail_deny

    sqs_client = await sync_to_async(boto3_cached_conn)(
        "sqs",
        service_type="client",
        region=queue_region,
        retry_max_attempts=2,
        account_number=queue_account_number,
        assume_role=queue_assume_role,
        client_kwargs=config.get("boto3.client_kwargs", {}),
    )

    queue_url_res = await sync_to_async(sqs_client.get_queue_url
                                        )(QueueName=queue_name)
    queue_url = queue_url_res.get("QueueUrl")
    if not queue_url:
        raise DataNotRetrievable(
            f"Unable to retrieve Queue URL for {queue_arn}")
    messages_awaitable = await sync_to_async(sqs_client.receive_message
                                             )(QueueUrl=queue_url,
                                               MaxNumberOfMessages=10)
    new_events = 0
    messages = messages_awaitable.get("Messages", [])
    num_events = 0
    reached_limit_on_num_messages_to_process = False

    while messages:
        if num_events >= max_num_messages_to_process:
            reached_limit_on_num_messages_to_process = True
            break
        processed_messages = []
        for message in messages:
            try:
                message_body = json.loads(message["Body"])
                try:
                    if "Message" in message_body:
                        decoded_message = json.loads(
                            message_body["Message"])["detail"]
                    else:
                        decoded_message = message_body["detail"]
                except Exception as e:
                    log.error({
                        **log_data,
                        "message": "Unable to process Cloudtrail message",
                        "message_body": message_body,
                        "error": str(e),
                    })
                    sentry_sdk.capture_exception()
                    continue
                event_name = decoded_message.get("eventName")
                event_source = decoded_message.get("eventSource")
                for event_source_substitution in config.get(
                        "event_bridge.detect_cloudtrail_denies_and_update_cache.event_bridge_substitutions",
                    [".amazonaws.com"],
                ):
                    event_source = event_source.replace(
                        event_source_substitution, "")
                event_time = decoded_message.get("eventTime")
                utc_time = datetime.strptime(event_time, "%Y-%m-%dT%H:%M:%SZ")
                epoch_event_time = int(
                    (utc_time - datetime(1970, 1, 1)).total_seconds())
                # Skip entries older than a day
                if int(time.time()) - 86400 > epoch_event_time:
                    continue
                try:
                    session_name = decoded_message["userIdentity"][
                        "arn"].split("/")[-1]
                except (
                        IndexError,
                        KeyError,
                ):  # If IAM user, there won't be a session name
                    session_name = ""
                try:
                    principal_arn = decoded_message["userIdentity"][
                        "sessionContext"]["sessionIssuer"]["arn"]
                except KeyError:  # Skip events without a parsable ARN
                    continue

                event_call = f"{event_source}:{event_name}"

                ct_event = dict(
                    error_code=decoded_message.get("errorCode"),
                    error_message=decoded_message.get("errorMessage"),
                    arn=principal_arn,
                    # principal_owner=owner,
                    session_name=session_name,
                    source_ip=decoded_message["sourceIPAddress"],
                    event_call=event_call,
                    epoch_event_time=epoch_event_time,
                    ttl=epoch_event_time + event_ttl,
                    count=1,
                )
                resource = await get_resource_from_cloudtrail_deny(
                    ct_event, decoded_message)
                ct_event["resource"] = resource
                request_id = f"{principal_arn}-{session_name}-{event_call}-{resource}"
                ct_event["request_id"] = request_id
                generated_policy = await generate_policy_from_cloudtrail_deny(
                    ct_event)
                if generated_policy:
                    ct_event["generated_policy"] = generated_policy

                if all_cloudtrail_denies.get(request_id):
                    existing_count = all_cloudtrail_denies[request_id].get(
                        "count", 1)
                    ct_event["count"] += existing_count
                    all_cloudtrail_denies[request_id] = ct_event
                else:
                    all_cloudtrail_denies[request_id] = ct_event
                    new_events += 1
                num_events += 1
            except Exception as e:
                log.error({**log_data, "error": str(e)}, exc_info=True)
                sentry_sdk.capture_exception()
            processed_messages.append({
                "Id":
                message["MessageId"],
                "ReceiptHandle":
                message["ReceiptHandle"],
            })
        if processed_messages:
            await sync_to_async(sqs_client.delete_message_batch
                                )(QueueUrl=queue_url,
                                  Entries=processed_messages)

        await sync_to_async(dynamo.batch_write_cloudtrail_events
                            )(all_cloudtrail_denies.values())
        messages_awaitable = await sync_to_async(sqs_client.receive_message
                                                 )(QueueUrl=queue_url,
                                                   MaxNumberOfMessages=10)
        messages = messages_awaitable.get("Messages", [])
    if reached_limit_on_num_messages_to_process:
        # We hit our limit. Let's spawn another task immediately to process remaining messages
        celery_app.send_task(
            "consoleme.celery_tasks.celery_tasks.cache_cloudtrail_denies", )
    log_data["message"] = "Successfully cached Cloudtrail Access Denies"
    log_data["num_events"] = num_events
    log_data["new_events"] = new_events
    log.debug(log_data)

    return log_data
Пример #23
0
class UserManagementHandler(BaseAPIV2Handler):
    """
    Handles creating and updating users. Only authorized users are allowed to access this endpoint.
    """
    def initialize(self):
        self.ddb = UserDynamoHandler()

    async def post(self):
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "message": "Create/Update User",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
            "ip": self.ip,
        }
        generic_error_message = "Unable to create/update user"
        log.debug(log_data)
        # Checks authz levels of current user
        if not can_admin_all(self.user, self.groups):
            errors = ["User is not authorized to access this endpoint."]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 403, "unauthorized",
                                                log_data)
            return
        request = UserManagementModel.parse_raw(self.request.body)
        log_data["requested_user"] = request.username
        if request.user_management_action.value == "create":
            log.debug({
                **log_data,
                "message": "Creating user",
                "requested_user": request.username,
                "requested_groups": request.groups,
            })

            # Fails if password is not strong enough.
            password_strength_errors = await check_password_strength(
                request.password)
            if password_strength_errors:
                await handle_generic_error_response(
                    self,
                    password_strength_errors["message"],
                    password_strength_errors["errors"],
                    403,
                    "weak_password",
                    log_data,
                )
                return

            self.ddb.create_user(
                request.username,
                request.password,
                request.groups,
            )
            res = WebResponse(
                status="success",
                status_code=200,
                message=f"Successfully created user {request.username}.",
            )
            self.write(res.json())
            return
        elif request.user_management_action.value == "update":
            log.debug({
                **log_data,
                "message": "Updating user",
                "requested_user": request.username,
                "requested_groups": request.groups,
            })

            if request.password:
                # Fails if password is not strong enough.
                password_strength_errors = await check_password_strength(
                    request.password)
                if password_strength_errors:
                    await handle_generic_error_response(
                        self,
                        password_strength_errors["message"],
                        password_strength_errors["errors"],
                        403,
                        "weak_password",
                        log_data,
                    )
                    return
            self.ddb.update_user(
                request.username,
                request.password,
                request.groups,
            )
            res = WebResponse(
                status="success",
                status_code=200,
                message=f"Successfully updated user {request.username}.",
            )
            self.write(res.json())
            return
        elif request.user_management_action.value == "delete":
            log.debug({
                **log_data,
                "message": "Deleting user",
                "requested_user": request.username,
            })
            self.ddb.delete_user(request.username, )
            res = WebResponse(
                status="success",
                status_code=200,
                message=f"Successfully deleted user {request.username}.",
            )
            self.write(res.json())
            return
        else:
            errors = ["Change type is not supported by this endpoint."]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 403, "invalid_request",
                                                log_data)
            return
Пример #24
0
from collections import defaultdict

import simplejson as json
from boto3.dynamodb.types import Binary  # noqa

from consoleme.config import config
from consoleme.lib.aws import get_iam_principal_owner, simulate_iam_principal_action
from consoleme.lib.cache import store_json_results_in_redis_and_s3
from consoleme.lib.dynamo import UserDynamoHandler
from consoleme.lib.json_encoder import SetEncoder
from consoleme.lib.notifications.models import (
    ConsoleMeUserNotification,
    ConsoleMeUserNotificationAction,
)

ddb = UserDynamoHandler()


class CloudTrail:
    async def process_cloudtrail_errors(
        self,
        aws,
        notification_ttl_seconds=config.get(
            "process_cloudtrail_errors.notification_ttl", 86400),
    ) -> object:
        """
        Processes Cloudtrail Errors that were cached by the `cache_cloudtrail_denies` celery task. Generates and returns
        count data. If configured, generates notifications to end-users based on policies that can be generated

        :param notification_ttl_seconds:
        :return:
Пример #25
0
    def test_create_user(self):
        from consoleme.config import config

        headers = {
            config.get("auth.user_header_name"):
            "*****@*****.**",
            config.get("auth.groups_header_name"): "groupa,groupb,groupc",
        }

        body = json.dumps({
            "user_management_action": "create",
            "username": "******",
            "password": "******",
            "groups": ["group1", "group2", "group3"],
        })
        response = self.fetch("/api/v2/user",
                              headers=headers,
                              method="POST",
                              body=body)
        self.assertEqual(response.code, 200)
        self.assertEqual(
            json.loads(response.body),
            {
                "status": "success",
                "reason": None,
                "redirect_url": None,
                "status_code": 200,
                "message": "Successfully created user testuser3.",
                "errors": None,
                "data": None,
            },
        )

        # Verify new user works
        from consoleme.lib.dynamo import UserDynamoHandler
        from consoleme.models import LoginAttemptModel

        ddb = UserDynamoHandler()
        login_attempt_success = LoginAttemptModel(username="******",
                                                  password="******")

        should_pass = async_to_sync(
            ddb.authenticate_user)(login_attempt_success)
        self.assertEqual(
            should_pass.dict(),
            {
                "authenticated": True,
                "errors": None,
                "username": "******",
                "groups": ["group1", "group2", "group3"],
            },
        )
        login_attempt_fail = LoginAttemptModel(username="******",
                                               password="******")

        should_fail = async_to_sync(ddb.authenticate_user)(login_attempt_fail)

        self.assertEqual(
            should_fail.dict(),
            {
                "authenticated":
                False,
                "errors": [
                    "User doesn't exist, or password is incorrect. ",
                    "Your next authentication failure will result in a 1 second wait. "
                    "This wait time will expire after 60 seconds of no authentication failures.",
                ],
                "username":
                None,
                "groups":
                None,
            },
        )

        # Update password
        body = json.dumps({
            "user_management_action": "update",
            "username": "******",
            "password": "******",
        })
        response = self.fetch("/api/v2/user",
                              headers=headers,
                              method="POST",
                              body=body)
        self.assertEqual(response.code, 200)
        self.assertEqual(
            json.loads(response.body),
            {
                "status": "success",
                "reason": None,
                "redirect_url": None,
                "status_code": 200,
                "message": "Successfully updated user testuser3.",
                "errors": None,
                "data": None,
            },
        )

        # Update groups
        body = json.dumps({
            "user_management_action": "update",
            "username": "******",
            "groups": ["group1", "group2", "group3", "newgroup"],
        })
        response = self.fetch("/api/v2/user",
                              headers=headers,
                              method="POST",
                              body=body)
        self.assertEqual(response.code, 200)
        self.assertEqual(
            json.loads(response.body),
            {
                "status": "success",
                "reason": None,
                "redirect_url": None,
                "status_code": 200,
                "message": "Successfully updated user testuser3.",
                "errors": None,
                "data": None,
            },
        )
        # Update groups and password AT THE SAME TIME!!1
        body = json.dumps({
            "user_management_action":
            "update",
            "username":
            "******",
            "password":
            "******",
            "groups": ["group1", "group2", "group3", "newgroup", "newgroup2"],
        })
        response = self.fetch("/api/v2/user",
                              headers=headers,
                              method="POST",
                              body=body)
        self.assertEqual(response.code, 200)
        self.assertEqual(
            json.loads(response.body),
            {
                "status": "success",
                "reason": None,
                "redirect_url": None,
                "status_code": 200,
                "message": "Successfully updated user testuser3.",
                "errors": None,
                "data": None,
            },
        )

        # Delete the user

        body = json.dumps({
            "user_management_action": "delete",
            "username": "******",
        })

        response = self.fetch("/api/v2/user",
                              headers=headers,
                              method="POST",
                              body=body)
        self.assertEqual(response.code, 200)
        self.assertEqual(
            json.loads(response.body),
            {
                "status": "success",
                "reason": None,
                "redirect_url": None,
                "status_code": 200,
                "message": "Successfully deleted user testuser3.",
                "errors": None,
                "data": None,
            },
        )
Пример #26
0
 def __init__(self, ) -> None:
     self.dynamo = UserDynamoHandler()
Пример #27
0
class UserRegistrationHandler(tornado.web.RequestHandler):
    """
    Allows user registration if it is configured.
    """
    def initialize(self):
        self.ddb = UserDynamoHandler()

    async def post(self):
        # TODO: Send verification e-mail to proposed user

        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "message": "Attempting to register user",
            "user-agent": self.request.headers.get("User-Agent"),
        }

        generic_error_message: str = "User registration failed"
        # Fail if getting users by password is not enabled
        if not config.get("auth.get_user_by_password"):
            errors = [
                "Expected configuration `auth.get_user_by_password`, but it is not enabled."
            ]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 403, "not_configured",
                                                log_data)
            return
        # Fail if user registration not allowed
        if not config.get("auth.allow_user_registration"):
            errors = [
                "Expected configuration `auth.allow_user_registration`, but it is not enabled."
            ]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 403, "not_configured",
                                                log_data)
            return

        registration_attempt = RegistrationAttemptModel.parse_raw(
            self.request.body)
        log_data["username"] = registration_attempt.username
        # Fail if username not valid email address
        try:
            if not validate_email(registration_attempt.username):
                errors = ["Username must be a valid e-mail address."]
                await handle_generic_error_response(
                    self,
                    generic_error_message,
                    errors,
                    403,
                    "invalid_request",
                    log_data,
                )
                return
        except Exception as e:
            sentry_sdk.capture_exception()
            await handle_generic_error_response(self, generic_error_message,
                                                [str(e)], 403,
                                                "invalid_request", log_data)
            return
        # Fail if user already exists
        if await self.ddb.get_user(registration_attempt.username):
            errors = ["User already exists"]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 403, "invalid_request",
                                                log_data)
            return

        # Fails if password is not strong enough.
        password_strength_errors = await check_password_strength(
            registration_attempt.password)
        if password_strength_errors:
            await handle_generic_error_response(
                self,
                password_strength_errors["message"],
                password_strength_errors["errors"],
                403,
                "weak_password",
                log_data,
            )
            return

        self.ddb.create_user(registration_attempt.username,
                             registration_attempt.password)

        res = WebResponse(
            status="success",
            status_code=200,
            message=
            f"Successfully created user {registration_attempt.username}.",
        )
        self.write(res.json())
Пример #28
0
 def initialize(self):
     self.ddb = UserDynamoHandler()
Пример #29
0
    async def get(self, request_id):
        """
        GET /api/v2/requests/{request_id}
        """
        tags = {"user": self.user}
        stats.count("RequestDetailHandler.get", tags=tags)
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "message": "Get request details",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
            "policy_request_id": request_id,
        }
        log.debug(log_data)

        if config.get("policy_editor.disallow_contractors",
                      True) and self.contractor:
            if self.user not in config.get(
                    "groups.can_bypass_contractor_restrictions", []):
                self.write_error(
                    403, message="Only FTEs are authorized to view this page.")
                return

        try:
            extended_request, last_updated = await self._get_extended_request(
                request_id, log_data)
        except InvalidRequestParameter as e:
            sentry_sdk.capture_exception(tags={"user": self.user})
            self.write_error(400, message="Error validating input: " + str(e))
            return
        except NoMatchingRequest as e:
            sentry_sdk.capture_exception(tags={"user": self.user})
            self.write_error(404, message="Error getting request:" + str(e))
            return
        # Run these tasks concurrently.
        concurrent_results = await asyncio.gather(
            populate_old_policies(extended_request, self.user),
            populate_cross_account_resource_policies(extended_request,
                                                     self.user),
        )
        extended_request = concurrent_results[0]

        populate_cross_account_resource_policies_result = concurrent_results[1]

        if populate_cross_account_resource_policies_result["changed"]:
            extended_request = populate_cross_account_resource_policies_result[
                "extended_request"]
            # Update in dynamo with the latest resource policy changes
            dynamo = UserDynamoHandler(self.user)
            updated_request = await dynamo.write_policy_request_v2(
                extended_request)
            last_updated = updated_request.get("last_updated")

        can_approve_reject = (can_admin_policies(self.user, self.groups), )
        can_update_cancel = await can_update_cancel_requests_v2(
            extended_request.requester_email, self.user, self.groups)
        can_move_back_to_pending = await can_move_back_to_pending_v2(
            extended_request, last_updated, self.user, self.groups)

        # In the future request_specific_config will have specific approvers for specific changes based on ABAC
        request_specific_config = {
            "can_approve_reject": can_approve_reject,
            "can_update_cancel": can_update_cancel,
            "can_move_back_to_pending": can_move_back_to_pending,
        }

        template = None
        # Force a refresh of the role in Redis/DDB
        arn_parsed = parse_arn(extended_request.arn)
        if arn_parsed["service"] == "iam" and arn_parsed["resource"] == "role":
            iam_role = await aws.fetch_iam_role(arn_parsed["account"],
                                                extended_request.arn)
            template = iam_role.get("templated")
        response = {
            "request": extended_request.json(),
            "last_updated": last_updated,
            "request_config": request_specific_config,
            "template": template,
        }

        self.write(response)
Пример #30
0
    async def post(self):
        """
        POST /api/v2/request

        Request example JSON: (Request Schema is RequestCreationModel in models.py)

        {
          "changes": {
            "changes": [
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "inline_policy",
                "action": "attach",
                "policy": {
                  "policy_document": {
                    "Version": "2012-10-17",
                    "Statement": [
                      {
                        "Action": [
                          "s3:ListMultipartUploadParts*",
                          "s3:ListBucket"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "arn:aws:s3:::curtis-nflx-test/*",
                          "arn:aws:s3:::curtis-nflx-test"
                        ],
                        "Sid": "cmccastrapel159494014dsd1shak"
                      },
                      {
                        "Action": [
                          "ec2:describevolumes",
                          "ec2:detachvolume",
                          "ec2:describelicenses",
                          "ec2:AssignIpv6Addresses",
                          "ec2:reportinstancestatus"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "*"
                        ],
                        "Sid": "cmccastrapel1594940141hlvvv"
                      },
                      {
                        "Action": [
                          "sts:AssumeRole"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "arn:aws:iam::123456789012:role/curtisTestInstanceProfile"
                        ],
                        "Sid": "cmccastrapel1596483596easdits"
                      }
                    ]
                  }
                }
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "assume_role_policy",
                "policy": {
                  "policy_document": {
                    "Statement": [
                      {
                        "Action": "sts:AssumeRole",
                        "Effect": "Allow",
                        "Principal": {
                          "AWS": "arn:aws:iam::123456789012:role/consolemeInstanceProfile"
                        },
                        "Sid": "AllowConsoleMeProdAssumeRolses"
                      }
                    ],
                    "Version": "2012-10-17"
                  }
                }
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "managed_policy",
                "policy_name": "ApiProtect",
                "action": "attach",
                "arn": "arn:aws:iam::123456789012:policy/ApiProtect"
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "managed_policy",
                "policy_name": "TagProtect",
                "action": "detach",
                "arn": "arn:aws:iam::123456789012:policy/TagProtect"
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "inline_policy",
                "policy_name": "random_policy254",
                "action": "attach",
                "policy": {
                  "policy_document": {
                    "Version": "2012-10-17",
                    "Statement": [
                      {
                        "Action": [
                          "ec2:AssignIpv6Addresses"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "*"
                        ],
                        "Sid": "cmccastrapel1594940141shakabcd"
                      }
                    ]
                  }
                }
              }
            ]
          },
          "justification": "testing this out.",
          "admin_auto_approve": false
        }

        Response example JSON: (Response Schema is RequestCreationResponse in models.py)

        {
            "errors": 1,
            "request_created": true,
            "request_id": "0c9fb298-c8ea-4d50-917c-3212da07b3ad",
            "action_results": [
                {
                    "status": "success",
                    "message": "Success description"
                },
                {
                    "status": "error",
                    "message": "Error description"
                }
            ]
        }


        """

        if config.get("policy_editor.disallow_contractors",
                      True) and self.contractor:
            if self.user not in config.get(
                    "groups.can_bypass_contractor_restrictions", []):
                raise MustBeFte("Only FTEs are authorized to view this page.")

        tags = {"user": self.user}
        stats.count("RequestHandler.post", tags=tags)
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "message": "Create request initialization",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
            "ip": self.ip,
            "admin_auto_approved": False,
            "probe_auto_approved": False,
        }
        log.debug(log_data)
        try:
            # Validate the model
            changes = RequestCreationModel.parse_raw(self.request.body)
            extended_request = await generate_request_from_change_model_array(
                changes, self.user)
            log_data["request"] = extended_request.dict()
            log.debug(log_data)
            admin_approved = False
            approval_probe_approved = False

            # TODO: Provide a note to the requester that admin_auto_approve will apply the requested policies only.
            # It will not automatically apply generated policies. The administrative user will need to visit the policy
            # Request page to do this manually.
            if changes.admin_auto_approve:
                # make sure user is allowed to use admin_auto_approve
                can_manage_policy_request = (can_admin_policies(
                    self.user, self.groups), )
                if can_manage_policy_request:
                    extended_request.request_status = RequestStatus.approved
                    admin_approved = True
                    extended_request.reviewer = self.user
                    self_approval_comment = CommentModel(
                        id=str(uuid.uuid4()),
                        timestamp=int(time.time()),
                        user_email=self.user,
                        user=extended_request.requester_info,
                        last_modified=int(time.time()),
                        text=f"Self-approved by admin: {self.user}",
                    )
                    extended_request.comments.append(self_approval_comment)
                    log_data["admin_auto_approved"] = True
                    log_data["request"] = extended_request.dict()
                    log.debug(log_data)
                    stats.count(
                        f"{log_data['function']}.post.admin_auto_approved",
                        tags={"user": self.user},
                    )
                else:
                    # someone is trying to use admin bypass without being an admin, don't allow request to proceed
                    stats.count(
                        f"{log_data['function']}.post.unauthorized_admin_bypass",
                        tags={"user": self.user},
                    )
                    log_data[
                        "message"] = "Unauthorized user trying to use admin bypass"
                    log.error(log_data)
                    await write_json_error("Unauthorized", obj=self)
                    return
            else:
                # If admin auto approve is false, check for auto-approve probe eligibility
                is_eligible_for_auto_approve_probe = (
                    await is_request_eligible_for_auto_approval(
                        extended_request, self.user))
                # If we have only made requests that are eligible for auto-approval probe, check against them
                if is_eligible_for_auto_approve_probe:
                    should_auto_approve_request = await should_auto_approve_policy_v2(
                        extended_request, self.user, self.groups)
                    if should_auto_approve_request["approved"]:
                        extended_request.request_status = RequestStatus.approved
                        approval_probe_approved = True
                        stats.count(
                            f"{log_data['function']}.probe_auto_approved",
                            tags={"user": self.user},
                        )
                        approving_probes = []
                        for approving_probe in should_auto_approve_request[
                                "approving_probes"]:
                            approving_probe_comment = CommentModel(
                                id=str(uuid.uuid4()),
                                timestamp=int(time.time()),
                                user_email=
                                f"Auto-Approve Probe: {approving_probe['name']}",
                                last_modified=int(time.time()),
                                text=
                                f"Policy {approving_probe['policy']} auto-approved by probe: {approving_probe['name']}",
                            )
                            extended_request.comments.append(
                                approving_probe_comment)
                            approving_probes.append(approving_probe["name"])
                        extended_request.reviewer = (
                            f"Auto-Approve Probe: {','.join(approving_probes)}"
                        )
                        log_data["probe_auto_approved"] = True
                        log_data["request"] = extended_request.dict()
                        log.debug(log_data)

            dynamo = UserDynamoHandler(self.user)
            request = await dynamo.write_policy_request_v2(extended_request)
            log_data["message"] = "New request created in Dynamo"
            log_data["request"] = extended_request.dict()
            log_data["dynamo_request"] = request
            log.debug(log_data)
        except (InvalidRequestParameter, ValidationError) as e:
            log_data["message"] = "Validation Exception"
            log.error(log_data, exc_info=True)
            stats.count(f"{log_data['function']}.validation_exception",
                        tags={"user": self.user})
            self.write_error(400, message="Error validating input: " + str(e))
            if config.get("development"):
                raise
            return
        except Exception as e:
            log_data[
                "message"] = "Unknown Exception occurred while parsing request"
            log.error(log_data, exc_info=True)
            stats.count(f"{log_data['function']}.exception",
                        tags={"user": self.user})
            sentry_sdk.capture_exception(tags={"user": self.user})
            self.write_error(500, message="Error parsing request: " + str(e))
            if config.get("development"):
                raise
            return

        # If here, request has been successfully created
        response = RequestCreationResponse(
            errors=0,
            request_created=True,
            request_id=extended_request.id,
            request_url=f"/policies/request/{extended_request.id}",
            action_results=[],
        )

        # If approved is true due to an auto-approval probe or admin auto-approval, apply the non-autogenerated changes
        if extended_request.request_status == RequestStatus.approved:
            for change in extended_request.changes.changes:
                if change.autogenerated:
                    continue
                policy_request_modification_model = (
                    PolicyRequestModificationRequestModel.parse_obj({
                        "modification_model": {
                            "command": "apply_change",
                            "change_id": change.id,
                        }
                    }))
                policy_apply_response = (
                    await parse_and_apply_policy_request_modification(
                        extended_request,
                        policy_request_modification_model,
                        self.user,
                        self.groups,
                        int(time.time()),
                        approval_probe_approved,
                    ))
                response.errors = policy_apply_response.errors
                response.action_results = policy_apply_response.action_results

            # Update in dynamo
            await dynamo.write_policy_request_v2(extended_request)
            account_id = await get_resource_account(extended_request.arn)

            # Force a refresh of the role in Redis/DDB
            arn_parsed = parse_arn(extended_request.arn)
            if arn_parsed["service"] == "iam" and arn_parsed[
                    "resource"] == "role":
                await aws.fetch_iam_role(account_id,
                                         extended_request.arn,
                                         force_refresh=True)
            log_data["request"] = extended_request.dict()
            log_data["message"] = "Applied changes based on approved request"
            log_data["response"] = response.dict()
            log.debug(log_data)

        await aws.send_communications_new_policy_request(
            extended_request, admin_approved, approval_probe_approved)
        self.write(response.json())
        await self.finish()
        await cache_all_policy_requests()
        return