def add_expiration( session, # type: Session expiration, # type: datetime group_name, # type: str member_name, # type: str recipients, # type: List[str] member_is_user, # type: bool ): # type: (...) -> None async_key = _expiration_key(group_name, member_name) send_after = expiration - timedelta(settings().expiration_notice_days) email_context = { "expiration": expiration, "group_name": group_name, "member_name": member_name, "member_is_user": member_is_user, } # type: Context send_async_email( session=session, recipients=recipients, subject="expiration warning for membership in group '{}'".format( group_name), template="expiration_warning", settings=settings(), context=email_context, send_after=send_after, async_key=async_key, )
def add_expiration( session, # type: Session expiration, # type: datetime group_name, # type: str member_name, # type: str recipients, # type: List[str] member_is_user, # type: bool ): # type: (...) -> None async_key = _expiration_key(group_name, member_name) send_after = expiration - timedelta(settings().expiration_notice_days) email_context = { "expiration": expiration, "group_name": group_name, "member_name": member_name, "member_is_user": member_is_user, } # type: Context send_async_email( session=session, recipients=recipients, subject="expiration warning for membership in group '{}'".format(group_name), template="expiration_warning", settings=settings(), context=email_context, send_after=send_after, async_key=async_key, )
def expiring_graph(session, graph, users, groups, permissions): # noqa: F811 now = datetime.utcnow() note_exp_now = now + timedelta(settings().expiration_notice_days) week = timedelta(7) add_member(groups["team-sre"], users["*****@*****.**"], role="owner") add_member(groups["team-sre"], users["*****@*****.**"], role="owner") add_member(groups["team-sre"], users["*****@*****.**"], expiration=note_exp_now + week) add_member(groups["team-sre"], users["*****@*****.**"]) add_member( groups["team-sre"], users["*****@*****.**"], role="owner", expiration=note_exp_now + week ) revoke_member(groups["team-sre"], users["*****@*****.**"]) grant_permission(groups["team-sre"], permissions["ssh"], argument="*") add_member(groups["serving-team"], users["*****@*****.**"], role="owner") add_member(groups["serving-team"], groups["team-sre"], expiration=note_exp_now + week) add_member(groups["serving-team"], groups["tech-ops"]) grant_permission(groups["serving-team"], permissions["audited"]) add_member(groups["tech-ops"], users["*****@*****.**"], role="owner") add_member(groups["tech-ops"], users["*****@*****.**"], expiration=note_exp_now + 2 * week) grant_permission(groups["tech-ops"], permissions["ssh"], argument="shell") return graph
def test_shell(session, users, http_client, base_url): # noqa: F811 settings().shell = [["/bin/bash", "bash"], ["/bin/zsh", "zsh"]] user = users["*****@*****.**"] assert not get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) user = User.get(session, name=user.username) fe_url = url(base_url, "/users/{}/shell".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"shell": "/bin/bash"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 user = User.get(session, name=user.username) assert (get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None), "The user should have shell metadata" assert (get_user_metadata_by_key( session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/bash") fe_url = url(base_url, "/users/{}/shell".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"shell": "/bin/fish"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 user = User.get(session, name=user.username) assert (get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None), "The user should have shell metadata" assert (get_user_metadata_by_key( session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/bash") fe_url = url(base_url, "/users/{}/shell".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"shell": "/bin/zsh"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 user = User.get(session, name=user.username) assert (get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None), "The user should have shell metadata" assert (get_user_metadata_by_key( session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/zsh")
def test_metadata(session, users, http_client, base_url): # noqa: F811 settings().metadata_options = { "favorite_food": [["pizza", "pizza"], ["kale", "kale"]] } user = users["*****@*****.**"] assert not get_user_metadata_by_key(session, user.id, "favorite_food") user = User.get(session, name=user.username) set_user_metadata(session, user.id, "favorite_food", "default") fe_url = url(base_url, "/users/{}/metadata/favorite_food".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"value": "pizza"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 assert get_user_metadata_by_key(session, user.id, "favorite_food").data_value == "pizza" fe_url = url(base_url, "/users/{}/metadata/favorite_food".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"value": "kale"}), headers={"X-Grouper-User": user.username}, ) assert get_user_metadata_by_key(session, user.id, "favorite_food").data_value == "kale" fe_url = url(base_url, "/users/{}/metadata/favorite_food".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"value": "donuts"}), headers={"X-Grouper-User": user.username}, ) assert get_user_metadata_by_key(session, user.id, "favorite_food").data_value == "kale"
def update_request(session: Session, request: PermissionRequest, user: User, new_status: str, comment: str) -> None: """Update a request. Args: session: Database session request: Request to update user: User making update new_status: New status comment: Comment to include with status change Raises: grouper.audit.UserNotAuditor in case we're trying to add an audited permission to a group without auditors """ if request.status == new_status: # nothing to do return # make sure the grant can happen if new_status == "actioned": if request.permission.audited: # will raise UserNotAuditor if no auditors are owners of the group assert_controllers_are_auditors(request.group) # all rows we add have the same timestamp now = datetime.utcnow() # new status change row permission_status_change = PermissionRequestStatusChange( request=request, user_id=user.id, from_status=request.status, to_status=new_status, change_at=now, ).add(session) session.flush() # new comment Comment( obj_type=OBJ_TYPES_IDX.index("PermissionRequestStatusChange"), obj_pk=permission_status_change.id, user_id=user.id, comment=comment, created_on=now, ).add(session) # update permissionRequest status request.status = new_status session.commit() if new_status == "actioned": # actually grant permission try: grant_permission(session, request.group.id, request.permission.id, request.argument) except IntegrityError: session.rollback() # audit log AuditLog.log( session, user.id, "update_perm_request", "updated permission request to status: {}".format(new_status), on_group_id=request.group_id, on_user_id=request.requester_id, on_permission_id=request.permission.id, ) session.commit() # send notification template_engine = EmailTemplateEngine(settings()) subject_template = template_engine.get_template( "email/pending_permission_request_subj.tmpl") subject = "Re: " + subject_template.render( permission=request.permission.name, group=request.group.name) if new_status == "actioned": email_template = "permission_request_actioned" else: email_template = "permission_request_cancelled" email_context = { "group_name": request.group.name, "action_taken_by": user.name, "reason": comment, "permission_name": request.permission.name, "argument": request.argument, } send_email(session, [request.requester.name], subject, email_template, settings(), email_context)
def create_request(session: Session, user: User, group: Group, permission: Permission, argument: str, reason: str) -> PermissionRequest: """Creates an permission request and sends notification to the responsible approvers. Args: session: Database session user: User requesting permission group: Group requested permission would be applied to permission: Permission in question to request argument: argument for the given permission reason: reason the permission should be granted Raises: RequestAlreadyExists: Trying to create a request that is already pending NoOwnersAvailable: No owner is available for the requested perm + arg. grouper.audit.UserNotAuditor: The group has owners that are not auditors """ # check if group already has perm + arg pair for _, existing_perm_name, _, existing_perm_argument, _ in group.my_permissions( ): if permission.name == existing_perm_name and argument == existing_perm_argument: raise RequestAlreadyGranted() # check if request already pending for this perm + arg pair existing_count = (session.query(PermissionRequest).filter( PermissionRequest.group_id == group.id, PermissionRequest.permission_id == permission.id, PermissionRequest.argument == argument, PermissionRequest.status == "pending", ).count()) if existing_count > 0: raise RequestAlreadyExists() # determine owner(s) owners_by_arg_by_perm = get_owners_by_grantable_permission( session, separate_global=True) owner_arg_list = get_owner_arg_list( session, permission, argument, owners_by_arg_by_perm=owners_by_arg_by_perm) if not owner_arg_list: raise NoOwnersAvailable() if permission.audited: # will raise UserNotAuditor if any owner of the group is not an auditor assert_controllers_are_auditors(group) pending_status = "pending" now = datetime.utcnow() # multiple steps to create the request request = PermissionRequest( requester_id=user.id, group_id=group.id, permission_id=permission.id, argument=argument, status=pending_status, requested_at=now, ).add(session) session.flush() request_status_change = PermissionRequestStatusChange( request=request, user=user, to_status=pending_status, change_at=now).add(session) session.flush() Comment( obj_type=OBJ_TYPES_IDX.index("PermissionRequestStatusChange"), obj_pk=request_status_change.id, user_id=user.id, comment=reason, created_on=now, ).add(session) # send notification email_context = { "user_name": user.name, "group_name": group.name, "permission_name": permission.name, "argument": argument, "reason": reason, "request_id": request.id, "references_header": request.reference_id, } # TODO: would be nicer if it told you which group you're an approver of # that's causing this notification mail_to = [] global_owners = owners_by_arg_by_perm[GLOBAL_OWNERS]["*"] non_wildcard_owners = [ grant for grant in owner_arg_list if grant[1] != "*" ] non_global_owners = [ grant for grant in owner_arg_list if grant[0] not in global_owners ] if any(non_wildcard_owners): # non-wildcard owners should get all the notifications mailto_owner_arg_list = non_wildcard_owners elif any(non_global_owners): mailto_owner_arg_list = non_global_owners else: # only the wildcards so they get the notifications mailto_owner_arg_list = owner_arg_list for owner, arg in mailto_owner_arg_list: if owner.email_address: mail_to.append(owner.email_address) else: mail_to.extend([u for t, u in owner.my_members() if t == "User"]) template_engine = EmailTemplateEngine(settings()) subject_template = template_engine.get_template( "email/pending_permission_request_subj.tmpl") subject = subject_template.render(permission=permission.name, group=group.name) send_email(session, set(mail_to), subject, "pending_permission_request", settings(), email_context) return request
def reference_id(self): # type: () -> str return reference_id(settings(), "permission", self)
def test_expiration_notifications( expiring_graph, session, users, groups, permissions # noqa: F811 ): now = datetime.utcnow() note_exp_now = now + timedelta(settings().expiration_notice_days) day = timedelta(1) week = timedelta(7) # What expirations are coming up in the next day? Next week? upcoming_expirations = _get_unsent_expirations(session, now + day) assert upcoming_expirations == [] upcoming_expirations = _get_unsent_expirations(session, now + week) assert sorted(upcoming_expirations) == [ # Group, subgroup, subgroup owners. ("serving-team", "team-sre", "*****@*****.**"), ("serving-team", "team-sre", "*****@*****.**"), # Group, user, user. ("team-sre", "*****@*****.**", "*****@*****.**"), ] # Make someone expire a week from now. edit_member(groups["team-sre"], users["*****@*****.**"], expiration=note_exp_now + week) upcoming_expirations = _get_unsent_expirations(session, now + week) assert sorted(upcoming_expirations) == [ # Group, subgroup, subgroup owners. ("serving-team", "team-sre", "*****@*****.**"), ("serving-team", "team-sre", "*****@*****.**"), # Group, user, user. ("team-sre", "*****@*****.**", "*****@*****.**"), ("team-sre", "*****@*****.**", "*****@*****.**"), ] # Now cancel that expiration. edit_member(groups["team-sre"], users["*****@*****.**"], expiration=None) upcoming_expirations = _get_unsent_expirations(session, now + week) assert sorted(upcoming_expirations) == [ # Group, subgroup, subgroup owners. ("serving-team", "team-sre", "*****@*****.**"), ("serving-team", "team-sre", "*****@*****.**"), # Group, user, user. ("team-sre", "*****@*****.**", "*****@*****.**"), ] # Make an ordinary member an owner. edit_member(groups["team-sre"], users["*****@*****.**"], role="owner") upcoming_expirations = _get_unsent_expirations(session, now + week) assert sorted(upcoming_expirations) == [ # Group, subgroup, subgroup owners. ("serving-team", "team-sre", "*****@*****.**"), ("serving-team", "team-sre", "*****@*****.**"), ("serving-team", "team-sre", "*****@*****.**"), # Group, user, user. ("team-sre", "*****@*****.**", "*****@*****.**"), ] # Make an owner an ordinary member. edit_member(groups["team-sre"], users["*****@*****.**"], role="member") upcoming_expirations = _get_unsent_expirations(session, now + week) assert sorted(upcoming_expirations) == [ # Group, subgroup, subgroup owners. ("serving-team", "team-sre", "*****@*****.**"), ("serving-team", "team-sre", "*****@*****.**"), # Group, user, user. ("team-sre", "*****@*****.**", "*****@*****.**"), ] # Send notices about expirations coming up in the next day, next week. notices_sent = process_async_emails(settings(), session, now + day, dry_run=True) assert notices_sent == 0 notices_sent = process_async_emails(settings(), session, now + week, dry_run=True) assert notices_sent == 3 # ("serving-team", "team-sre", "*****@*****.**") # ("serving-team", "team-sre", "*****@*****.**") # ("team-sre", "*****@*****.**", "*****@*****.**") # Notices in the upcoming week have already been sent, but there's another # two weeks from now. upcoming_expirations = _get_unsent_expirations(session, now + week) assert upcoming_expirations == [] upcoming_expirations = _get_unsent_expirations(session, now + 2 * week) assert upcoming_expirations == [("tech-ops", "*****@*****.**", "*****@*****.**")] # We already sent these notices. notices_sent = process_async_emails(settings(), session, now + week, dry_run=True) assert notices_sent == 0 # Extend gary's membership to beyond worth mentioning expiration in two weeks. add_member(groups["tech-ops"], users["*****@*****.**"], expiration=note_exp_now + 3 * week) upcoming_expirations = _get_unsent_expirations(session, now + 2 * week) assert upcoming_expirations == [] notices_sent = process_async_emails(settings(), session, now + 2 * week, dry_run=True) assert notices_sent == 0
def test_shell(session, users, http_client, base_url): # noqa: F811 settings().shell = [["/bin/bash", "bash"], ["/bin/zsh", "zsh"]] user = users["*****@*****.**"] assert not get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) user = User.get(session, name=user.username) fe_url = url(base_url, "/users/{}/shell".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"shell": "/bin/bash"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 user = User.get(session, name=user.username) assert ( get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None ), "The user should have shell metadata" assert ( get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/bash" ) fe_url = url(base_url, "/users/{}/shell".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"shell": "/bin/fish"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 user = User.get(session, name=user.username) assert ( get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None ), "The user should have shell metadata" assert ( get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/bash" ) fe_url = url(base_url, "/users/{}/shell".format(user.username)) resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"shell": "/bin/zsh"}), headers={"X-Grouper-User": user.username}, ) assert resp.code == 200 user = User.get(session, name=user.username) assert ( get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None ), "The user should have shell metadata" assert ( get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/zsh" )
def reference_id(self): # type: () -> str return reference_id(settings(), "group", self)
def update_request( session, # type: Session request, # type: PermissionRequest user, # type: User new_status, # type: str comment, # type: str ): # type: (...) -> None """Update a request. Args: session(sqlalchemy.orm.session.Session): database session request(models.PermissionRequest): request to update user(models.User): user making update new_status(models.base.constants.REQUEST_STATUS_CHOICES): new status comment(str): comment to include with status change Raises: grouper.audit.UserNotAuditor in case we're trying to add an audited permission to a group without auditors """ if request.status == new_status: # nothing to do return # make sure the grant can happen if new_status == "actioned": if request.permission.audited: # will raise UserNotAuditor if no auditors are owners of the group assert_controllers_are_auditors(request.group) # all rows we add have the same timestamp now = datetime.utcnow() # new status change row permission_status_change = PermissionRequestStatusChange( request=request, user_id=user.id, from_status=request.status, to_status=new_status, change_at=now, ).add(session) session.flush() # new comment Comment( obj_type=OBJ_TYPES_IDX.index("PermissionRequestStatusChange"), obj_pk=permission_status_change.id, user_id=user.id, comment=comment, created_on=now, ).add(session) # update permissionRequest status request.status = new_status session.commit() if new_status == "actioned": # actually grant permission try: grant_permission(session, request.group.id, request.permission.id, request.argument) except IntegrityError: session.rollback() # audit log AuditLog.log( session, user.id, "update_perm_request", "updated permission request to status: {}".format(new_status), on_group_id=request.group_id, on_user_id=request.requester_id, on_permission_id=request.permission.id, ) session.commit() # send notification template_engine = EmailTemplateEngine(settings()) subject_template = template_engine.get_template("email/pending_permission_request_subj.tmpl") subject = "Re: " + subject_template.render( permission=request.permission.name, group=request.group.name ) if new_status == "actioned": email_template = "permission_request_actioned" else: email_template = "permission_request_cancelled" email_context = { "group_name": request.group.name, "action_taken_by": user.name, "reason": comment, "permission_name": request.permission.name, "argument": request.argument, } send_email( session, [request.requester.name], subject, email_template, settings(), email_context )
def create_request(session, user, group, permission, argument, reason): # type: (Session, User, Group, Permission, str, str) -> PermissionRequest """ Creates an permission request and sends notification to the responsible approvers. Args: session(sqlalchemy.orm.session.Session): database session user(models.User): user requesting permission group(models.Group): group requested permission would be applied to permission(models.Permission): permission in question to request argument(str): argument for the given permission reason(str): reason the permission should be granted Raises: RequestAlreadyExists if trying to create a request that is already pending NoOwnersAvailable if no owner is available for the requested perm + arg. grouper.audit.UserNotAuditor if the group has owners that are not auditors """ # check if group already has perm + arg pair for _, existing_perm_name, _, existing_perm_argument, _ in group.my_permissions(): if permission.name == existing_perm_name and argument == existing_perm_argument: raise RequestAlreadyGranted() # check if request already pending for this perm + arg pair existing_count = ( session.query(PermissionRequest) .filter( PermissionRequest.group_id == group.id, PermissionRequest.permission_id == permission.id, PermissionRequest.argument == argument, PermissionRequest.status == "pending", ) .count() ) if existing_count > 0: raise RequestAlreadyExists() # determine owner(s) owners_by_arg_by_perm = get_owners_by_grantable_permission(session, separate_global=True) owner_arg_list = get_owner_arg_list( session, permission, argument, owners_by_arg_by_perm=owners_by_arg_by_perm ) if not owner_arg_list: raise NoOwnersAvailable() if permission.audited: # will raise UserNotAuditor if any owner of the group is not an auditor assert_controllers_are_auditors(group) pending_status = "pending" now = datetime.utcnow() # multiple steps to create the request request = PermissionRequest( requester_id=user.id, group_id=group.id, permission_id=permission.id, argument=argument, status=pending_status, requested_at=now, ).add(session) session.flush() request_status_change = PermissionRequestStatusChange( request=request, user=user, to_status=pending_status, change_at=now ).add(session) session.flush() Comment( obj_type=OBJ_TYPES_IDX.index("PermissionRequestStatusChange"), obj_pk=request_status_change.id, user_id=user.id, comment=reason, created_on=now, ).add(session) # send notification email_context = { "user_name": user.name, "group_name": group.name, "permission_name": permission.name, "argument": argument, "reason": reason, "request_id": request.id, "references_header": request.reference_id, } # TODO: would be nicer if it told you which group you're an approver of # that's causing this notification mail_to = [] global_owners = owners_by_arg_by_perm[GLOBAL_OWNERS]["*"] non_wildcard_owners = [grant for grant in owner_arg_list if grant[1] != "*"] non_global_owners = [grant for grant in owner_arg_list if grant[0] not in global_owners] if any(non_wildcard_owners): # non-wildcard owners should get all the notifications mailto_owner_arg_list = non_wildcard_owners elif any(non_global_owners): mailto_owner_arg_list = non_global_owners else: # only the wildcards so they get the notifications mailto_owner_arg_list = owner_arg_list for owner, arg in mailto_owner_arg_list: if owner.email_address: mail_to.append(owner.email_address) else: mail_to.extend([u for t, u in owner.my_members() if t == "User"]) template_engine = EmailTemplateEngine(settings()) subject_template = template_engine.get_template("email/pending_permission_request_subj.tmpl") subject = subject_template.render(permission=permission.name, group=group.name) send_email( session, set(mail_to), subject, "pending_permission_request", settings(), email_context ) return request