Пример #1
0
def notify_nonauditor_promoted(settings, session, user, auditors_group, group_names):
    # type: (Settings, Session, User, Group, Set[str]) -> None
    """Send notification that a nonauditor has been promoted to be an auditor.

    Handles email notification and audit logging.

    Args:
        settings: Grouper Settings object for current run.
        session: Object for db session.
        user: The user that has been promoted.
        auditors_group: The auditors group
        group_names: The audited groups in which the user was previously a non-auditor approver.
    """
    member_name = user.username
    recipients = [member_name]
    auditors_group_name = auditors_group.groupname

    audit_data = {
        "action": "nonauditor_promoted",
        "actor_id": user.id,
        "description": "Added {} to group {}".format(member_name, auditors_group_name),
    }
    AuditLog.log(session, on_user_id=user.id, on_group_id=auditors_group.id, **audit_data)

    email_context = {"auditors_group_name": auditors_group_name, "member_name": member_name}
    send_email(
        session=session,
        recipients=recipients,
        subject='Added as member to group "{}"'.format(auditors_group_name),
        template="nonauditor_promoted",
        settings=settings,
        context=email_context,
    )
Пример #2
0
    def edit_member(self, requester, user_or_group, reason, **kwargs):
        """ Edit an existing member (User or Group) of a group.

            This takes the same parameters as add_member, except that we do not allow you to set
            a status: this only works on existing members.

            Any option that is not passed is not updated, and instead, the existing value for this
            user is kept.
        """
        logging.debug(
            "Editing member (%s) in %s", user_or_group.name, self.groupname
        )

        persist_group_member_changes(
            session=self.session,
            group=self,
            requester=requester,
            member=user_or_group,
            status="actioned",
            reason=reason,
            **kwargs
        )

        member_type = user_or_group.member_type

        message = "Edit member {} {}: {}".format(
            OBJ_TYPES_IDX[member_type].lower(), user_or_group.name, reason)
        AuditLog.log(self.session, requester.id, 'edit_member',
                     message, on_group_id=self.id)
Пример #3
0
def disable_permission_auditing(session, permission_name, actor_user_id):
    """Set a permission as audited.

    Args:
        session(models.base.session.Session): database session
        permission_name(str): name of permission in question
        actor_user_id(int): id of user who is disabling auditing
    """
    permission = get_permission(session, permission_name)
    if not permission:
        raise NoSuchPermission(name=permission_name)

    permission.audited = False

    AuditLog.log(
        session,
        actor_user_id,
        "disable_auditing",
        "Disabled auditing.",
        on_permission_id=permission.id,
    )

    Counter.incr(session, "updates")

    session.commit()
Пример #4
0
def create_service_account(session, actor, name, description, machine_set, owner):
    # type: (Session, User, str, str, str, Group) -> ServiceAccount
    """Creates a service account and its underlying user.

    Also adds the service account to the list of accounts managed by the owning group.

    Throws:
        BadMachineSet: if some plugin rejected the machine set
        DuplicateServiceAccount: if a user with the given name already exists
    """
    user = User(username=name, is_service_account=True)
    service_account = ServiceAccount(user=user, description=description, machine_set=machine_set)

    if machine_set is not None:
        _check_machine_set(service_account, machine_set)

    try:
        user.add(session)
        service_account.add(session)
        session.flush()
    except IntegrityError:
        session.rollback()
        raise DuplicateServiceAccount("User {} already exists".format(name))

    # Counter is updated here and the session is committed, so we don't need an additional update
    # or commit for the account creation.
    add_service_account(session, owner, service_account)

    AuditLog.log(session, actor.id, "create_service_account", "Created new service account.",
                 on_group_id=owner.id, on_user_id=service_account.user_id)

    return service_account
Пример #5
0
    def post(self, user_id=None, name=None):
        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = UserEnableForm(self.request.arguments)
        if not form.validate():
            # TODO: add error message
            return self.redirect("/users/{}?refresh=yes".format(user.name))

        if user.role_user:
            enable_service_account(self.session, actor=self.current_user,
                preserve_membership=form.preserve_membership.data, user=user)
        else:
            enable_user(self.session, user, self.current_user,
                preserve_membership=form.preserve_membership.data)

        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'enable_user',
                     'Enabled user.', on_user_id=user.id)

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #6
0
    def post(self, user_id=None, name=None, key_id=None, tag_id=None):
        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        try:
            key = get_public_key(self.session, user.id, key_id)
        except KeyNotFound:
            return self.notfound()

        tag = PublicKeyTag.get(self.session, id=tag_id)

        if not tag:
            return self.notfound()

        try:
            remove_tag_from_public_key(self.session, key, tag)
        except TagNotOnKey:
            return self.redirect("/users/{}?refresh=yes".format(user.name))

        AuditLog.log(self.session, self.current_user.id, 'untag_public_key',
                     'Untagged public key: {}'.format(key.fingerprint),
                     on_tag_id=tag.id, on_user_id=user.id)

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #7
0
    def post(self, user_id=None, name=None):

        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        try:
            if user.role_user:
                disable_role_user(self.session, user=user)
            else:
                disable_user(self.session, user)
        except PluginRejectedDisablingUser as e:
            alert = Alert("danger", str(e))
            return self.redirect("/users/{}".format(user.name), alerts=[alert])

        self.session.commit()

        AuditLog.log(
            self.session,
            self.current_user.id,
            "disable_user",
            "Disabled user.",
            on_user_id=user.id,
        )

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #8
0
    def post(self, user_id=None, name=None):
        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = UserShellForm(self.request.arguments)
        form.shell.choices = settings.shell
        if not form.validate():
            return self.render(
                "user-shell.html", form=form, user=user,
                alerts=self.get_form_alerts(form.errors),
            )

        user.set_metadata(USER_METADATA_SHELL_KEY, form.data["shell"])
        user.add(self.session)
        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'changed_shell',
                     'Changed shell: {}'.format(form.data["shell"]),
                     on_user_id=user.id)

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #9
0
    def post(self, user_id=None, name=None, key_id=None):
        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        try:
            key = get_public_key(self.session, user.id, key_id)
            delete_public_key(self.session, user.id, key_id)
        except KeyNotFound:
            return self.notfound()

        AuditLog.log(self.session, self.current_user.id, 'delete_public_key',
                     'Deleted public key: {}'.format(key.fingerprint),
                     on_user_id=user.id)

        email_context = {
                "actioner": self.current_user.name,
                "changed_user": user.name,
                "action": "removed",
                }
        send_email(self.session, [user.name], 'Public SSH key removed', 'ssh_keys_changed',
                settings, email_context)

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #10
0
def create_role_user(session, actor, name, description, canjoin):
    # type (Session, User, str, str, str) -> None
    """DEPRECATED: Do not use in production code

    Creates a service account with the given name, description, and canjoin status

    Args:
        session: the database session
        actor: the user creating the service account
        name: the name of the service account
        description: description of the service account
        canjoin: the canjoin status for management of the service account

    Throws:
        IntegrityError: if a user or group with the given name already exists
    """
    user = User(username=name, role_user=True)
    group = Group(groupname=name, description=description, canjoin=canjoin)

    user.add(session)
    group.add(session)

    group.add_member(actor, actor, "Group Creator", "actioned", None, "np-owner")
    group.add_member(actor, user, "Service Account", "actioned", None, "member")
    session.commit()

    AuditLog.log(
        session,
        actor.id,
        "create_role_user",
        "Created new service account.",
        on_group_id=group.id,
        on_user_id=user.id,
    )
Пример #11
0
    def post(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        user_id = kwargs.get("user_id")  # type: Optional[int]
        name = kwargs.get("name")  # type: Optional[str]

        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = UserGitHubForm(self.request.arguments)
        if not form.validate():
            return self.render(
                "user-github.html", form=form, user=user, alerts=self.get_form_alerts(form.errors)
            )

        new_username = form.data["username"]
        if new_username == "":
            new_username = None
        set_user_metadata(self.session, user.id, USER_METADATA_GITHUB_USERNAME_KEY, new_username)

        AuditLog.log(
            self.session,
            self.current_user.id,
            "changed_github_username",
            "Changed GitHub username: {}".format(form.data["username"]),
            on_user_id=user.id,
        )

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #12
0
    def post(self, tag_id=None, name=None):
        tag = PublicKeyTag.get(self.session, tag_id, name)
        if not tag:
            return self.notfound()

        if not user_has_permission(self.session, self.current_user, TAG_EDIT, tag.name):
            return self.forbidden()

        form = TagEditForm(self.request.arguments, obj=tag)
        if not form.validate():
            return self.render(
                "tag-edit.html", tag=tag, form=form,
                alerts=self.get_form_alerts(form.errors)
            )

        tag.description = form.data["description"]
        tag.enabled = form.data["enabled"]
        Counter.incr(self.session, "updates")

        try:
            self.session.commit()
        except IntegrityError:
            self.session.rollback()
            form.tagname.errors.append(
                "{} already exists".format(form.data["tagname"])
            )
            return self.render(
                "tag-edit.html", tag=tag, form=form,
                alerts=self.get_form_alerts(form.errors)
            )

        AuditLog.log(self.session, self.current_user.id, 'edit_tag',
                     'Edited tag.', on_tag_id=tag.id)

        return self.redirect("/tags/{}".format(tag.name))
Пример #13
0
def test_group_logdump(session, tmpdir, users, groups):  # noqa: F811
    groupname = "team-sre"
    group_id = groups[groupname].id

    yesterday = date.today() - timedelta(days=1)
    fn = tmpdir.join("out.csv").strpath

    call_main(
        session, tmpdir, "group", "log_dump", groupname, yesterday.isoformat(), "--outfile", fn
    )
    with open(fn, "r") as fh:
        out = fh.read()

    assert not out, "nothing yet"

    AuditLog.log(
        session, users["*****@*****.**"].id, "make_noise", "making some noise", on_group_id=group_id
    )
    session.commit()

    call_main(
        session, tmpdir, "group", "log_dump", groupname, yesterday.isoformat(), "--outfile", fn
    )
    with open(fn, "r") as fh:
        entries = [x for x in csv.reader(fh)]

    assert len(entries) == 1, "should capture our new audit log entry"

    log_time, actor, description, action, extra = entries[0]
    assert groupname in extra
Пример #14
0
    def post(self, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        if not self.current_user.can_manage(group):
            return self.forbidden()

        form = GroupRemoveForm(self.request.arguments)
        if not form.validate():
            return self.send_error(status_code=400)

        member_type, member_name = form.data["member_type"], form.data["member"]

        members = group.my_members()
        if not members.get((member_type.capitalize(), member_name), None):
            return self.notfound()

        removed_member = get_user_or_group(self.session, member_name, user_or_group=member_type)

        if self.current_user == removed_member:
            return self.send_error(
                status_code=400,
                reason="Can't remove yourself. Leave group instead."
            )

        group.revoke_member(self.current_user, removed_member, "Removed by owner/np-owner/manager")
        AuditLog.log(self.session, self.current_user.id, 'remove_from_group',
                     '{} was removed from the group.'.format(removed_member.name),
                     on_group_id=group.id, on_user_id=removed_member.id)
        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #15
0
    def post(self, name=None, mapping_id=None):
        grantable = self.current_user.my_grantable_permissions()
        if not grantable:
            return self.forbidden()

        mapping = PermissionMap.get(self.session, id=mapping_id)
        if not mapping:
            return self.notfound()

        allowed = False
        for perm in grantable:
            if perm[0].name == mapping.permission.name:
                if matches_glob(perm[1], mapping.argument):
                    allowed = True
        if not allowed:
            return self.forbidden()

        permission = mapping.permission
        group = mapping.group

        mapping.delete(self.session)
        Counter.incr(self.session, "updates")
        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'revoke_permission',
                     'Revoked permission with argument: {}'.format(mapping.argument),
                     on_group_id=group.id, on_permission_id=permission.id)

        return self.redirect('/groups/{}?refresh=yes'.format(group.name))
Пример #16
0
def test_group_logdump(make_session, session, users, groups, tmpdir):
    make_session.return_value = session

    groupname = 'team-sre'
    group_id = groups[groupname].id

    yesterday = date.today() - timedelta(days=1)
    fn = tmpdir.join('out.csv').strpath

    call_main('group', 'log_dump', groupname, yesterday.isoformat(), '--outfile', fn)
    with open(fn, 'r') as fh:
        out = fh.read()

    assert not out, 'nothing yet'

    AuditLog.log(session, users['*****@*****.**'].id, 'make_noise', 'making some noise',
            on_group_id=group_id)
    session.commit()

    call_main('group', 'log_dump', groupname, yesterday.isoformat(), '--outfile', fn)
    with open(fn, 'r') as fh:
        entries = [x for x in csv.reader(fh)]

    assert len(entries) == 1, 'should capture our new audit log entry'

    log_time, actor, description, action, extra = entries[0]
    assert groupname in extra
Пример #17
0
    def post(self, user_id=None, name=None):
        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = UserPasswordForm(self.request.arguments)
        if not form.validate():
            return self.render("user-password-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors))

        pass_name = form.data["name"]
        password = form.data["password"]
        try:
            add_new_user_password(self.session, pass_name, password, user.id)
        except PasswordAlreadyExists:
            self.session.rollback()
            form.name.errors.append("Name already in use.")
            return self.render("user-password-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors))

        AuditLog.log(
            self.session,
            self.current_user.id,
            "add_password",
            "Added password: {}".format(pass_name),
            on_user_id=user.id,
        )

        email_context = {"actioner": self.current_user.name, "changed_user": user.name, "pass_name": pass_name}
        send_email(self.session, [user.name], "User password created", "user_password_created", settings, email_context)
        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #18
0
    def post(self, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        members = group.my_members()
        if not user_role(self.current_user, members) in ("owner", "np-owner"):
            return self.forbidden()

        # Enabling and disabling service accounts via the group endpoints is forbidden
        # because we need the preserve_membership data that is only available via the
        # UserEnable form.
        if is_role_user(self.session, group=group):
            return self.forbidden()

        group.disable()

        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'disable_group',
                     'Disabled group.', on_group_id=group.id)

        if group.audit:
            # complete the audit
            group.audit.complete = True
            self.session.commit()

            AuditLog.log(self.session, self.current_user.id, 'complete_audit',
                         'Disabling group completes group audit.', on_group_id=group.id)

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #19
0
    def post(self):
        form = TagCreateForm(self.request.arguments)
        if not form.validate():
            return self.render(
                "tag-create.html", form=form,
                alerts=self.get_form_alerts(form.errors)
            )

        tag = PublicKeyTag(
            name=form.data["tagname"],
            description=form.data["description"],
        )

        try:
            tag.add(self.session)
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            form.tagname.errors.append(
                "{} already exists".format(form.data["tagname"])
            )
            return self.render(
                "tag-create.html", form=form,
                alerts=self.get_form_alerts(form.errors)
            )

        Counter.incr(self.session, "updates")
        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'create_tag',
                     'Created new tag.', on_tag_id=tag.id)

        return self.redirect("/tags/{}?refresh=yes".format(tag.name))
    def post(self, group_id=None, name=None, account_id=None, accountname=None, mapping_id=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()
        service_account = ServiceAccount.get(self.session, account_id, accountname)
        if not service_account:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, service_account):
            return self.forbidden()

        mapping = ServiceAccountPermissionMap.get(self.session, mapping_id)
        if not mapping:
            return self.notfound()

        permission = mapping.permission
        argument = mapping.argument

        mapping.delete(self.session)
        Counter.incr(self.session, "updates")
        self.session.commit()

        AuditLog.log(
            self.session,
            self.current_user.id,
            "revoke_permission",
            "Revoked permission with argument: {}".format(argument),
            on_permission_id=permission.id,
            on_group_id=group.id,
            on_user_id=service_account.user.id,
        )

        return self.redirect(
            "/groups/{}/service/{}?refresh=yes".format(group.name, service_account.user.username)
        )
Пример #21
0
    def post(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        user_id = kwargs.get("user_id")  # type: Optional[int]
        name = kwargs.get("name")  # type: Optional[str]

        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = UserShellForm(self.request.arguments)
        form.shell.choices = settings().shell
        if not form.validate():
            return self.render(
                "user-shell.html", form=form, user=user, alerts=self.get_form_alerts(form.errors)
            )

        set_user_metadata(self.session, user.id, USER_METADATA_SHELL_KEY, form.data["shell"])

        AuditLog.log(
            self.session,
            self.current_user.id,
            "changed_shell",
            "Changed shell: {}".format(form.data["shell"]),
            on_user_id=user.id,
        )

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #22
0
    def post(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        user_id = kwargs.get("user_id")  # type: Optional[int]
        name = kwargs.get("name")  # type: Optional[str]

        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = UserTokenForm(self.request.arguments)
        if not form.validate():
            return self.render(
                "user-token-add.html",
                form=form,
                user=user,
                alerts=self.get_form_alerts(form.errors),
            )

        try:
            token, secret = add_new_user_token(
                self.session, UserToken(name=form.data["name"], user=user)
            )
            self.session.commit()
        except IntegrityError:
            self.session.rollback()
            form.name.errors.append("Name already in use.")
            return self.render(
                "user-token-add.html",
                form=form,
                user=user,
                alerts=self.get_form_alerts(form.errors),
            )

        AuditLog.log(
            self.session,
            self.current_user.id,
            "add_token",
            "Added token: {}".format(token.name),
            on_user_id=user.id,
        )

        email_context = {
            "actioner": self.current_user.name,
            "changed_user": user.name,
            "action": "added",
        }
        send_email(
            self.session,
            [user.name],
            "User token created",
            "user_tokens_changed",
            settings(),
            email_context,
        )
        return self.render("user-token-created.html", token=token, secret=secret)
Пример #23
0
    def post(self):
        can_create = user_creatable_permissions(self.session, self.current_user)
        if not can_create:
            return self.forbidden()

        form = PermissionCreateForm(self.request.arguments)
        if not form.validate():
            return self.render(
                "permission-create.html", form=form, alerts=self.get_form_alerts(form.errors)
            )

        # A user is allowed to create a permission if the name matches any of the globs that they
        # are given access to via PERMISSION_CREATE, as long as the permission does not match a
        # reserved name. (Unless specifically granted.)
        allowed = False
        for creatable in can_create:
            if matches_glob(creatable, form.data["name"]):
                allowed = True

        for failure_message in test_reserved_names(form.data["name"]):
            form.name.errors.append(failure_message)

        if not allowed:
            form.name.errors.append("Permission name does not match any of your allowed patterns.")

        if form.name.errors:
            return self.render(
                "permission-create.html", form=form, alerts=self.get_form_alerts(form.errors)
            )

        try:
            permission = create_permission(
                self.session, form.data["name"], form.data["description"]
            )
            self.session.flush()
        except IntegrityError:
            self.session.rollback()
            form.name.errors.append("Name already in use. Permissions must be unique.")
            return self.render(
                "permission-create.html",
                form=form,
                can_create=sorted(can_create),
                alerts=self.get_form_alerts(form.errors),
            )

        self.session.commit()

        AuditLog.log(
            self.session,
            self.current_user.id,
            "create_permission",
            "Created permission.",
            on_permission_id=permission.id,
        )

        # No explicit refresh because handler queries SQL.
        return self.redirect("/permissions/{}".format(permission.name))
    def post(self, group_id=None, name=None, account_id=None, accountname=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()
        service_account = ServiceAccount.get(self.session, account_id, accountname)
        if not service_account:
            return self.notfound()
        user = service_account.user

        if not self.check_access(self.session, self.current_user, service_account):
            return self.forbidden()

        grantable = group.my_permissions()
        form = self.get_form(grantable)
        if not form.validate():
            return self.render(
                "service-account-permission-grant.html", form=form, user=user, group=group,
                alerts=self.get_form_alerts(form.errors)
            )

        permission = Permission.get(self.session, form.data["permission"])
        if not permission:
            return self.notfound()

        allowed = False
        for perm in grantable:
            if perm[1] == permission.name:
                if matches_glob(perm[3], form.data["argument"]):
                    allowed = True
                    break
        if not allowed:
            form.argument.errors.append(
                "The group {} does not have that permission".format(group.name))
            return self.render(
                "service-account-permission-grant.html", form=form, user=user, group=group,
                alerts=self.get_form_alerts(form.errors)
            )

        try:
            grant_permission_to_service_account(
                self.session, service_account, permission, form.data["argument"])
        except IntegrityError:
            self.session.rollback()
            return self.render(
                "service-account-permission-grant.html", form=form, user=user,
                alerts=self.get_form_alerts(form.errors)
            )

        AuditLog.log(self.session, self.current_user.id, "grant_permission",
                     "Granted permission with argument: {}".format(form.data["argument"]),
                     on_permission_id=permission.id, on_group_id=group.id,
                     on_user_id=service_account.user.id)

        return self.redirect("/groups/{}/service/{}?refresh=yes".format(
            group.name, service_account.user.username))
Пример #25
0
    def post(self, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        if not user_can_manage_group(self.session, group, self.current_user):
            return self.forbidden()

        form = GroupRemoveForm(self.request.arguments)
        if not form.validate():
            return self.send_error(status_code=400)

        member_type, member_name = form.data["member_type"], form.data["member"]

        members = group.my_members()
        if not members.get((member_type.capitalize(), member_name), None):
            return self.notfound()

        removed_member = get_user_or_group(self.session, member_name, user_or_group=member_type)

        if self.current_user == removed_member:
            return self.send_error(
                status_code=400,
                reason="Can't remove yourself. Leave group instead."
            )

        role_user = is_role_user(self.session, group=group)

        if (role_user and
                get_role_user(self.session, group=group).user.name == removed_member.name):
            return self.send_error(
                status_code=400,
                reason="Can't remove a service account user from the service account group."
            )

        try:
            group.revoke_member(
                self.current_user,
                removed_member,
                "Removed by owner/np-owner/manager"
            )

            AuditLog.log(self.session, self.current_user.id, 'remove_from_group',
                         '{} was removed from the group.'.format(removed_member.name),
                         on_group_id=group.id, on_user_id=removed_member.id)
        except PluginRejectedGroupMembershipUpdate as e:
            alert = Alert("danger", str(e))

            if role_user:
                return self.redirect("/service/{}".format(group.name), alerts=[alert])
            else:
                return self.redirect("/groups/{}".format(group.name), alerts=[alert])

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #26
0
def enable_service_account(session, actor, service_account, owner):
    # type: (Session, User, ServiceAccount, Group) -> None
    """Enables a service account and sets a new owner."""
    enable_user(session, service_account.user, actor, preserve_membership=False)
    add_service_account(session, owner, service_account)

    AuditLog.log(session, actor.id, "enable_service_account", "Enabled service account.",
                 on_group_id=owner.id, on_user_id=service_account.user_id)

    Counter.incr(session, "updates")
    session.commit()
Пример #27
0
    def post(self, user_id=None, name=None, token_id=None):
        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if (user.name != self.current_user.name) and not self.current_user.user_admin:
            return self.forbidden()

        token = UserToken.get(self.session, user=user, id=token_id)
        disable_user_token(self.session, token)
        AuditLog.log(self.session, self.current_user.id, 'disable_token',
                     'Disabled token: {}'.format(token.name),
                     on_user_id=user.id)
        self.session.commit()
        return self.render("user-token-disabled.html", token=token)
Пример #28
0
def disable_service_account(session, actor, service_account):
    # type: (Session, User, ServiceAccount) -> None
    """Disables a service account and deletes the association with a Group."""
    disable_user(session, service_account.user)
    owner_id = service_account.owner.group.id
    service_account.owner.delete(session)
    permissions = session.query(ServiceAccountPermissionMap).filter_by(
        service_account_id=service_account.id)
    for permission in permissions:
        permission.delete(session)

    AuditLog.log(session, actor.id, "disable_service_account", "Disabled service account.",
                 on_group_id=owner_id, on_user_id=service_account.user_id)

    Counter.incr(session, "updates")
    session.commit()
Пример #29
0
    def post(self, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        members = group.my_members()
        if not self.current_user.my_role(members) in ("owner", "np-owner"):
            return self.forbidden()

        group.enable()
        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'enable_group',
                     'Enabled group.', on_group_id=group.id)

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #30
0
    def post(self, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        members = group.my_members()
        if not user_role(self.current_user, members):
            return self.forbidden()

        group.revoke_member(self.current_user, self.current_user, "User self-revoked.")

        AuditLog.log(self.session, self.current_user.id, 'leave_group',
                     '{} left the group.'.format(self.current_user.name),
                     on_group_id=group.id)

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #31
0
def test_promote_nonauditors(
        mock_gagn,
        standard_graph,
        graph,
        users,
        groups,
        session,
        permissions  # noqa: F811
):
    """ Test expiration auditing and notification. """

    assert graph.get_group_details("audited-team")["audited"]

    #
    # Ensure auditors promotion for all approvers
    #
    approver_roles = ["owner", "np-owner", "manager"]

    affected_users = set(
        ["*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**"])
    for idx, role in enumerate(approver_roles):

        # Add non-auditor as an approver to an audited group
        add_member(groups["audited-team"], users["*****@*****.**"], role=role)
        graph.update_from_db(session)
        assert not affected_users.intersection(get_users(graph, "auditors"))

        # do the promotion logic
        settings = BackgroundSettings()
        set_global_settings(settings)
        background = BackgroundProcessor(settings, None)
        background.promote_nonauditors(session)

        # Check that the users now added to auditors group
        graph.update_from_db(session)
        assert affected_users.intersection(get_users(
            graph, "auditors")) == affected_users
        unsent_emails = _get_unsent_emails_and_send(session)
        assert any([
            'Subject: Added as member to group "auditors"' in email.body
            and "To: [email protected]" in email.body for email in unsent_emails
        ])
        assert any([
            'Subject: Added as member to group "auditors"' in email.body
            and "To: [email protected]" in email.body for email in unsent_emails
        ])
        assert any([
            'Subject: Added as member to group "auditors"' in email.body
            and "To: [email protected]" in email.body for email in unsent_emails
        ])

        audits = AuditLog.get_entries(session, action="nonauditor_promoted")
        assert len(audits) == len(affected_users) * (idx + 1)

        # reset for next iteration
        revoke_member(groups["audited-team"], users["*****@*****.**"])
        for username in affected_users:
            revoke_member(groups["auditors"], users[username])

    #
    # Ensure nonauditor, nonapprovers in audited groups do not get promoted
    #

    # first, run a promotion to get any other promotion that we don't
    # care about out of the way
    background = BackgroundProcessor(settings, None)
    background.promote_nonauditors(session)

    prev_audit_log_count = len(
        AuditLog.get_entries(session, action="nonauditor_promoted"))

    member_roles = ["member"]
    for idx, role in enumerate(member_roles):

        # Add non-auditor as a non-approver to an audited group
        add_member(groups["audited-team"], users["*****@*****.**"], role=role)

        # do the promotion logic
        background = BackgroundProcessor(settings, None)
        background.promote_nonauditors(session)

        # Check that the user is not added to auditors group
        graph.update_from_db(session)
        assert "*****@*****.**" not in get_users(graph, "auditors")

        assert not any([
            'Subject: Added as member to group "auditors"' in email.body
            and "To: [email protected]" in email.body
            for email in _get_unsent_emails_and_send(session)
        ])

        audits = AuditLog.get_entries(session, action="nonauditor_promoted")
        assert len(audits) == prev_audit_log_count

        revoke_member(groups["audited-team"], users["*****@*****.**"])
Пример #32
0
def notify_edge_expiration(settings, session, edge):
    """Send notification that an edge has expired.

    Handles email notification and audit logging.

    Args:
        settings (Settings): Grouper Settings object for current run.
        session (Session): Object for db session.
        edge (GroupEdge): The expiring edge.
    """
    # TODO(herb): get around circular depdendencies; long term remove call to
    # send_async_email() from grouper.models
    from grouper.model_soup import Group
    from grouper.models.base.constants import OBJ_TYPES_IDX
    from grouper.models.user import User

    # TODO(rra): Arbitrarily use the first listed owner of the group from which membership expired
    # as the actor, since we have to provide an actor and we didn't record who set the expiration on
    # the edge originally.
    actor_id = next(edge.group.my_owners().itervalues()).id

    # Pull data about the edge and the affected user or group.
    group_name = edge.group.name
    if OBJ_TYPES_IDX[edge.member_type] == "User":
        user = User.get(session, pk=edge.member_pk)
        member_name = user.username
        recipients = [member_name]
        member_is_user = True
    else:
        subgroup = Group.get(session, pk=edge.member_pk)
        member_name = subgroup.groupname
        recipients = subgroup.my_owners_as_strings()
        member_is_user = False

    # Log to the audit log.  How depends on whether a user's membership has expired or a group's
    # membership has expired.
    audit_data = {
        "action": "expired_from_group",
        "actor_id": actor_id,
        "description": "{} expired out of the group".format(member_name),
    }
    if member_is_user:
        AuditLog.log(session, on_user_id=user.id, on_group_id=edge.group_id, **audit_data)
    else:
        # Make an audit log entry for both the subgroup and the parent group so that it will show up
        # in the FE view for both groups.
        AuditLog.log(session, on_group_id=edge.group_id, **audit_data)
        AuditLog.log(session, on_group_id=subgroup.id, **audit_data)

    # Send email notification to the affected people.
    email_context = {
        "group_name": group_name,
        "member_name": member_name,
        "member_is_user": member_is_user,
    }
    send_email(
        session=session,
        recipients=recipients,
        subject="Membership in {} expired".format(group_name),
        template="expiration",
        settings=settings,
        context=email_context,
    )
Пример #33
0
def user_command(args):
    # type: (Namespace) -> None
    session = make_session()

    if args.subcommand == "create":
        for username in args.username:
            user = User.get(session, name=username)
            if not user:
                logging.info("{}: No such user, creating...".format(username))
                user = User.get_or_create(session,
                                          username=username,
                                          role_user=args.role_user)
                session.commit()
            else:
                logging.info(
                    "{}: Already exists. Doing nothing.".format(username))
        return

    elif args.subcommand == "disable":
        for username in args.username:
            user = User.get(session, name=username)
            if not user:
                logging.info(
                    "{}: No such user. Doing nothing.".format(username))
            elif not user.enabled:
                logging.info(
                    "{}: User already disabled. Doing nothing.".format(
                        username))
            else:
                logging.info("{}: User found, disabling...".format(username))
                try:
                    if user.role_user:
                        disable_role_user(session, user)
                    else:
                        disable_user(session, user)
                    AuditLog.log(
                        session,
                        user.id,
                        "disable_user",
                        "(Administrative) User disabled via grouper-ctl",
                        on_user_id=user.id,
                    )
                    session.commit()
                except PluginRejectedDisablingUser as e:
                    logging.error(e.message)

        return

    elif args.subcommand == "enable":
        for username in args.username:
            user = User.get(session, name=username)
            if not user:
                logging.info(
                    "{}: No such user. Doing nothing.".format(username))
            elif user.enabled:
                logging.info(
                    "{}: User not disabled. Doing nothing.".format(username))
            else:
                logging.info("{}: User found, enabling...".format(username))
                if user.role_user:
                    enable_role_user(
                        session,
                        user,
                        preserve_membership=args.preserve_membership,
                        user=user)
                else:
                    enable_user(session,
                                user,
                                user,
                                preserve_membership=args.preserve_membership)
                AuditLog.log(
                    session,
                    user.id,
                    "enable_user",
                    "(Administrative) User enabled via grouper-ctl",
                    on_user_id=user.id,
                )
                session.commit()
        return

    # "add_public_key" and "set_metadata"
    user = User.get(session, name=args.username)
    if not user:
        logging.error("{}: No such user. Doing nothing.".format(args.username))
        return

    # User must exist at this point.

    if args.subcommand == "set_metadata":
        logging.info("Setting %s metadata: %s=%s", args.username,
                     args.metadata_key, args.metadata_value)
        if args.metadata_value == "":
            args.metadata_value = None
        set_user_metadata(session, user.id, args.metadata_key,
                          args.metadata_value)
        session.commit()
    elif args.subcommand == "add_public_key":
        logging.info("Adding public key for user")

        try:
            pubkey = public_key.add_public_key(session, user, args.public_key)
        except public_key.DuplicateKey:
            logging.error("Key already in use")
            return
        except public_key.PublicKeyParseError:
            logging.error("Public key appears to be invalid")
            return

        AuditLog.log(
            session,
            user.id,
            "add_public_key",
            "(Administrative) Added public key: {}".format(
                pubkey.fingerprint_sha256),
            on_user_id=user.id,
        )
Пример #34
0
    def post(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        group_id = kwargs.get("group_id")  # type: Optional[int]
        name = kwargs.get("name")  # type: Optional[str]

        group = Group.get(self.session, group_id, name)
        if not group or not group.enabled:
            return self.notfound()

        form = GroupJoinForm(self.request.arguments)
        form.member.choices = self._get_choices(group)
        if not form.validate():
            return self.render("group-join.html",
                               form=form,
                               group=group,
                               alerts=self.get_form_alerts(form.errors))

        member = self._get_member(form.data["member"])
        if not member:
            return self.render(
                "group-join.html",
                form=form,
                group=group,
                alerts=[
                    Alert(
                        "danger", "Unknown user or group: {}".format(
                            form.data["member"]))
                ],
            )

        fail_message = "This join is denied with this role at this time."
        try:
            user_can_join = assert_can_join(group,
                                            member,
                                            role=form.data["role"])
        except UserNotAuditor as e:
            user_can_join = False
            fail_message = str(e)
        if not user_can_join:
            return self.render(
                "group-join.html",
                form=form,
                group=group,
                alerts=[
                    Alert("danger", fail_message, "Audit Policy Enforcement")
                ],
            )

        if group.canjoin == "nobody":
            fail_message = "This group cannot be joined at this time."
            return self.render("group-join.html",
                               form=form,
                               group=group,
                               alerts=[Alert("danger", fail_message)])

        if group.require_clickthru_tojoin:
            if not form.data["clickthru_agreement"]:
                return self.render(
                    "group-join.html",
                    form=form,
                    group=group,
                    alerts=[
                        Alert(
                            "danger",
                            "please accept review of the group's description",
                            "Clickthru Enforcement",
                        )
                    ],
                )

        # We only use the default expiration time if no expiration time was given
        # This does mean that if a user wishes to join a group with no expiration
        # (even with an owner's permission) that has an auto expiration, they must
        # first be accepted to the group and then have the owner edit the user to
        # have no expiration.

        expiration = None
        if form.data["expiration"]:
            expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y")
        elif group.auto_expire:
            expiration = datetime.utcnow() + group.auto_expire

        request = group.add_member(
            requester=self.current_user,
            user_or_group=member,
            reason=form.data["reason"],
            status=GROUP_JOIN_CHOICES[group.canjoin],
            expiration=expiration,
            role=form.data["role"],
        )
        self.session.commit()

        if group.canjoin == "canask":
            AuditLog.log(
                self.session,
                self.current_user.id,
                "join_group",
                "{} requested to join with role: {}".format(
                    member.name, form.data["role"]),
                on_group_id=group.id,
            )

            mail_to = [
                user.name for user in group.my_users()
                if GROUP_EDGE_ROLES[user.role] in ("manager", "owner",
                                                   "np-owner")
            ]

            email_context = {
                "requester": member.name,
                "requested_by": self.current_user.name,
                "request_id": request.id,
                "group_name": group.name,
                "reason": form.data["reason"],
                "expiration": expiration,
                "role": form.data["role"],
                "references_header": request.reference_id,
            }

            subj = self.render_template("email/pending_request_subj.tmpl",
                                        group=group.name,
                                        user=self.current_user.name)
            send_email(self.session, mail_to, subj, "pending_request",
                       settings(), email_context)

        elif group.canjoin == "canjoin":
            AuditLog.log(
                self.session,
                self.current_user.id,
                "join_group",
                "{} auto-approved to join with role: {}".format(
                    member.name, form.data["role"]),
                on_group_id=group.id,
            )
        else:
            raise Exception("Need to update the GroupJoin.post audit logging")

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #35
0
    def post(self, name=None):
        grantable = user_grantable_permissions(self.session, self.current_user)
        if not grantable:
            return self.forbidden()

        group = Group.get(self.session, None, name)
        if not group:
            return self.notfound()

        form = PermissionGrantForm(self.request.arguments)
        form.permission.choices = [["", "(select one)"]]
        for perm in grantable:
            grantable_str = "{} ({})".format(perm[0].name, perm[1])
            form.permission.choices.append([perm[0].name, grantable_str])

        if not form.validate():
            return self.render("permission-grant.html",
                               form=form,
                               group=group,
                               alerts=self.get_form_alerts(form.errors))

        permission = Permission.get(self.session, form.data["permission"])
        if not permission:
            return self.notfound()  # Shouldn't happen.

        allowed = False
        for perm in grantable:
            if perm[0].name == permission.name:
                if matches_glob(perm[1], form.data["argument"]):
                    allowed = True
                    break
        if not allowed:
            form.argument.errors.append(
                "You do not have grant authority over that permission/argument combination."
            )
            return self.render(
                "permission-grant.html",
                form=form,
                group=group,
                alerts=self.get_form_alerts(form.errors),
            )

        # If the permission is audited, then see if the subtree meets auditing requirements.
        if permission.audited:
            fail_message = (
                "Permission is audited and this group (or a subgroup) contains "
                +
                "owners, np-owners, or managers who have not received audit training."
            )
            try:
                permission_ok = assert_controllers_are_auditors(group)
            except UserNotAuditor as e:
                permission_ok = False
                fail_message = e
            if not permission_ok:
                form.permission.errors.append(fail_message)
                return self.render(
                    "permission-grant.html",
                    form=form,
                    group=group,
                    alerts=self.get_form_alerts(form.errors),
                )

        try:
            grant_permission(self.session,
                             group.id,
                             permission.id,
                             argument=form.data["argument"])
        except IntegrityError:
            self.session.rollback()
            form.argument.errors.append(
                "Permission and Argument already mapped to this group.")
            return self.render(
                "permission-grant.html",
                form=form,
                group=group,
                alerts=self.get_form_alerts(form.errors),
            )

        self.session.commit()

        AuditLog.log(self.session,
                     self.current_user.id,
                     'grant_permission',
                     'Granted permission with argument: {}'.format(
                         form.data["argument"]),
                     on_permission_id=permission.id,
                     on_group_id=group.id)

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #36
0
def test_github(session, users, http_client, base_url, mocker):  # noqa: F811
    user = users["*****@*****.**"]
    assert get_user_metadata_by_key(session, user.id,
                                    USER_METADATA_GITHUB_USERNAME_KEY) is None

    user = User.get(session, name=user.username)
    fe_url = url(base_url, "/github/link_begin/{}".format(user.id))
    mocker.patch.object(settings(), "github_app_client_id", "a-client-id")
    resp = yield http_client.fetch(
        fe_url,
        method="GET",
        headers={"X-Grouper-User": user.username},
        follow_redirects=False,
        raise_error=False,
    )
    assert resp.code == 302
    redir_url = urlparse(resp.headers["Location"])
    assert redir_url.netloc == "github.com"
    assert redir_url.path == "/login/oauth/authorize"
    query_params = parse_qs(redir_url.query)
    assert query_params["client_id"] == ["a-client-id"]
    state, = query_params["state"]
    assert "github-link-state={}".format(state) in resp.headers["Set-cookie"]
    assert query_params["redirect_uri"] == [
        "http://127.0.0.1:8888/github/link_complete/{}".format(user.id)
    ]

    fe_url = url(
        base_url, "/github/link_complete/{}?code=tempcode&state={}".format(
            user.id, state))
    with pytest.raises(HTTPError) as excinfo:
        yield http_client.fetch(
            fe_url,
            method="GET",
            headers={
                "X-Grouper-User": user.username,
                "Cookie": "github-link-state=bogus-state"
            },
        )
    assert excinfo.value.code == 400

    recorder = FakeGitHubHttpClient()
    proxy_plugin = PluginProxy([SecretPlugin()])
    mocker.patch("grouper.fe.handlers.github._get_github_http_client",
                 lambda: recorder)
    mocker.patch("grouper.fe.handlers.github.get_plugin_proxy",
                 lambda: proxy_plugin)
    mocker.patch.object(settings(), "http_proxy_host", "proxy-server")
    mocker.patch.object(settings(), "http_proxy_port", 42)
    resp = yield http_client.fetch(
        fe_url,
        method="GET",
        headers={
            "X-Grouper-User": user.username,
            "Cookie": "github-link-state=" + state
        },
    )
    authorize_request, user_request = recorder.requests
    assert authorize_request.proxy_host == "proxy-server"
    assert authorize_request.proxy_port == 42
    assert user_request.proxy_host == "proxy-server"
    assert user_request.proxy_port == 42
    authorize_params = parse_qs(authorize_request.body)
    assert authorize_params[b"code"] == [b"tempcode"]
    assert authorize_params[b"state"] == [state.encode("ascii")]
    assert authorize_params[b"client_id"] == [b"a-client-id"]
    assert authorize_params[b"client_secret"] == [b"client-secret"]
    assert user_request.headers["Authorization"] == "token a-access-token"
    assert (get_user_metadata_by_key(session, user.id,
                                     USER_METADATA_GITHUB_USERNAME_KEY)
            is not None)
    assert (get_user_metadata_by_key(
        session, user.id,
        USER_METADATA_GITHUB_USERNAME_KEY).data_value == "zorkian-on-gh")

    audit_entries = AuditLog.get_entries(session,
                                         on_user_id=user.id,
                                         action="changed_github_username")
    assert len(audit_entries) == 1
    assert audit_entries[
        0].description == "Changed GitHub username: zorkian-on-gh"

    fe_url = url(base_url, "/users/{}/github/clear".format(user.username))
    resp = yield http_client.fetch(fe_url,
                                   method="POST",
                                   headers={"X-Grouper-User": user.username},
                                   body=b"")
    assert resp.code == 200
    assert get_user_metadata_by_key(session, user.id,
                                    USER_METADATA_GITHUB_USERNAME_KEY) is None

    audit_entries = AuditLog.get_entries(session,
                                         on_user_id=user.id,
                                         action="changed_github_username")
    assert len(audit_entries) == 2
    audit_entries.sort(key=operator.attrgetter("id"))
    assert audit_entries[1].description == "Cleared GitHub link"
Пример #37
0
    def post(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        form = AuditCreateForm(self.request.arguments)
        if not form.validate():
            return self.render(
                "audit-create.html", form=form, alerts=self.get_form_alerts(form.errors)
            )

        if not user_has_permission(self.session, self.current_user, AUDIT_MANAGER):
            return self.forbidden()

        # Step 1, detect if there are non-completed audits and fail if so.
        open_audits = self.session.query(Audit).filter(Audit.complete == False).all()
        if open_audits:
            raise Exception("Sorry, there are audits in progress.")
        ends_at = datetime.strptime(form.data["ends_at"], "%m/%d/%Y")

        # Step 2, find all audited groups and schedule audits for each.
        audited_groups = []
        for groupname in self.graph.groups:
            if not self.graph.get_group_details(groupname)["audited"]:
                continue
            group = Group.get(self.session, name=groupname)
            audit = Audit(group_id=group.id, ends_at=ends_at)
            try:
                audit.add(self.session)
                self.session.flush()
            except IntegrityError:
                self.session.rollback()
                raise Exception("Failed to start the audit. Please try again.")

            # Update group with new audit
            audited_groups.append(group)
            group.audit_id = audit.id

            # Step 3, now get all members of this group and set up audit rows for those edges.
            for member in itervalues(group.my_members()):
                auditmember = AuditMember(audit_id=audit.id, edge_id=member.edge_id)
                try:
                    auditmember.add(self.session)
                except IntegrityError:
                    self.session.rollback()
                    raise Exception("Failed to start the audit. Please try again.")

        self.session.commit()

        AuditLog.log(
            self.session,
            self.current_user.id,
            "start_audit",
            "Started global audit.",
            category=AuditLogCategory.audit,
        )

        # Calculate schedule of emails, basically we send emails at various periods in advance
        # of the end of the audit period.
        schedule_times = []
        not_before = datetime.utcnow() + timedelta(1)
        for days_prior in (28, 21, 14, 7, 3, 1):
            email_time = ends_at - timedelta(days_prior)
            email_time.replace(hour=17, minute=0, second=0)
            if email_time > not_before:
                schedule_times.append((days_prior, email_time))

        # Now send some emails. We do this separately/later to ensure that the audits are all
        # created. Email notifications are sent multiple times if group audits are still
        # outstanding.
        for group in audited_groups:
            mail_to = [
                member.name
                for member in group.my_users()
                if GROUP_EDGE_ROLES[member.role] in ("owner", "np-owner")
            ]

            send_email(
                self.session,
                mail_to,
                "Group Audit: {}".format(group.name),
                "audit_notice",
                settings(),
                {"group": group.name, "ends_at": ends_at},
            )

            for days_prior, email_time in schedule_times:
                send_async_email(
                    self.session,
                    mail_to,
                    "Group Audit: {} - {} day(s) left".format(group.name, days_prior),
                    "audit_notice_reminder",
                    settings(),
                    {"group": group.name, "ends_at": ends_at, "days_left": days_prior},
                    email_time,
                    async_key="audit-{}".format(group.id),
                )

        return self.redirect("/audits")
Пример #38
0
    def my_log_entries(self):

        return AuditLog.get_entries(self.session,
                                    on_group_id=self.id,
                                    limit=20)
Пример #39
0
    def post(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        user_id = kwargs.get("user_id")  # type: Optional[int]
        name = kwargs.get("name")  # type: Optional[str]

        user = User.get(self.session, user_id, name)
        if not user:
            return self.notfound()

        if not self.check_access(self.session, self.current_user, user):
            return self.forbidden()

        form = PublicKeyForm(self.request.arguments)
        if not form.validate():
            return self.render(
                "public-key-add.html",
                form=form,
                user=user,
                alerts=self.get_form_alerts(form.errors),
            )

        try:
            pubkey = public_key.add_public_key(self.session, user, form.data["public_key"])
        except public_key.DuplicateKey:
            form.public_key.errors.append("Key already in use. Public keys must be unique.")
        except public_key.PublicKeyParseError:
            form.public_key.errors.append("Public key appears to be invalid.")
        except public_key.BadPublicKey as e:
            form.public_key.errors.append(str(e))

        if form.public_key.errors:
            return self.render(
                "public-key-add.html",
                form=form,
                user=user,
                alerts=self.get_form_alerts(form.errors),
            )

        AuditLog.log(
            self.session,
            self.current_user.id,
            "add_public_key",
            "Added public key: {}".format(pubkey.fingerprint_sha256),
            on_user_id=user.id,
        )

        email_context = {
            "actioner": self.current_user.name,
            "changed_user": user.name,
            "action": "added",
        }
        send_email(
            self.session,
            [user.name],
            "Public SSH key added",
            "ssh_keys_changed",
            settings(),
            email_context,
        )

        return self.redirect("/users/{}?refresh=yes".format(user.name))
Пример #40
0
def update_request(session, request, user, new_status, comment):
    """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)

    session.commit()

    # send notification
    if new_status == "actioned":
        subject = "Request for Permission Actioned"
        email_template = "permission_request_actioned"
    else:
        subject = "Request for Permission Cancelled"
        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)
Пример #41
0
def test_expire_nonauditors(standard_graph, users, groups, session,
                            permissions):
    """ Test expiration auditing and notification. """

    graph = standard_graph  # noqa

    # Test audit autoexpiration for all approvers

    approver_roles = ["owner", "np-owner", "manager"]

    for role in approver_roles:

        # Add non-auditor as an owner to an audited group
        add_member(groups["audited-team"], users["*****@*****.**"], role=role)
        session.commit()
        graph.update_from_db(session)

        group_md = graph.get_group_details("audited-team")

        assert group_md.get('audited', False)

        # Expire the edges.
        background = BackgroundThread(settings, None)
        background.expire_nonauditors(session)

        # Check that the edges are now marked as inactive.
        edge = session.query(GroupEdge).filter_by(
            group_id=groups["audited-team"].id,
            member_pk=users["*****@*****.**"].id).scalar()
        assert edge.expiration is not None
        assert edge.expiration < datetime.utcnow() + timedelta(
            days=settings.nonauditor_expiration_days)
        assert edge.expiration > datetime.utcnow() + timedelta(
            days=settings.nonauditor_expiration_days - 1)

        assert any([
            "Subject: Membership in audited-team set to expire" in email.body
            and "To: [email protected]" in email.body
            for email in _get_unsent_emails_and_send(session)
        ])

        audits = AuditLog.get_entries(session, action="nonauditor_flagged")
        assert len(audits) == 3 + 1 * (approver_roles.index(role) + 1)

        revoke_member(groups["audited-team"], users["*****@*****.**"])

    # Ensure nonauditor, nonapprovers in audited groups do not get set to expired

    member_roles = ["member"]

    for role in member_roles:

        # Add non-auditor as an owner to an audited group
        add_member(groups["audited-team"], users["*****@*****.**"], role=role)
        session.commit()
        graph.update_from_db(session)

        group_md = graph.get_group_details("audited-team")

        assert group_md.get('audited', False)

        # Expire the edges.
        background = BackgroundThread(settings, None)
        background.expire_nonauditors(session)

        # Check that the edges are now marked as inactive.
        edge = session.query(GroupEdge).filter_by(
            group_id=groups["audited-team"].id,
            member_pk=users["*****@*****.**"].id).scalar()
        assert edge.expiration is None

        assert not any([
            "Subject: Membership in audited-team set to expire" in email.body
            and "To: [email protected]" in email.body
            for email in _get_unsent_emails_and_send(session)
        ])

        audits = AuditLog.get_entries(session, action="nonauditor_flagged")
        assert len(audits) == 3 + 1 * len(approver_roles)

        revoke_member(groups["audited-team"], users["*****@*****.**"])
Пример #42
0
    def post(self, audit_id):
        if not user_has_permission(self.session, self.current_user,
                                   PERMISSION_AUDITOR):
            return self.forbidden()

        audit = self.session.query(Audit).filter(Audit.id == audit_id).one()

        # only owners can complete
        owner_ids = {member.id for member in audit.group.my_owners().values()}
        if self.current_user.id not in owner_ids:
            return self.forbidden()

        if audit.complete:
            return self.redirect("/groups/{}".format(audit.group.name))

        edges = {}
        for argument in self.request.arguments:
            if argument.startswith("audit_"):
                edges[int(argument.split("_")
                          [1])] = self.request.arguments[argument][0].decode()

        for audit_member_info in get_group_audit_members_infos(
                self.session, audit.group):
            if audit_member_info.audit_member_obj.id in edges:
                # You can only approve yourself (otherwise you can remove yourself
                # from the group and leave it ownerless)
                if audit_member_info.member_obj.id == self.current_user.id:
                    audit_member_info.audit_member_obj.status = "approved"
                elif edges[audit_member_info.audit_member_obj.
                           id] in AUDIT_STATUS_CHOICES:
                    audit_member_info.audit_member_obj.status = edges[
                        audit_member_info.audit_member_obj.id]

        self.session.commit()

        # If there are still pending statuses, then redirect to the group page.
        if group_has_pending_audit_members(self.session, audit.group):
            return self.redirect("/groups/{}".format(audit.group.name))

        # Complete audits have to be "enacted" now. This means anybody marked as remove has to
        # be removed from the group now.
        try:
            for audit_member_info in get_group_audit_members_infos(
                    self.session, audit.group):
                member_obj = audit_member_info.member_obj
                if audit_member_info.audit_member_obj.status == "remove":
                    audit.group.revoke_member(self.current_user, member_obj,
                                              "Revoked as part of audit.")
                    AuditLog.log(
                        self.session,
                        self.current_user.id,
                        "remove_member",
                        "Removed membership in audit: {}".format(
                            member_obj.name),
                        on_group_id=audit.group.id,
                        on_user_id=member_obj.id,
                        category=AuditLogCategory.audit,
                    )
        except PluginRejectedGroupMembershipUpdate as e:
            alert = Alert("danger", str(e))
            return self.redirect("/groups/{}".format(audit.group.name),
                                 alerts=[alert])

        audit.complete = True
        self.session.commit()

        # Now cancel pending emails
        cancel_async_emails(self.session, "audit-{}".format(audit.group.id))

        AuditLog.log(
            self.session,
            self.current_user.id,
            "complete_audit",
            "Completed group audit.",
            on_group_id=audit.group.id,
            category=AuditLogCategory.audit,
        )

        # check if all audits are complete
        if get_audits(self.session, only_open=True).count() == 0:
            AuditLog.log(
                self.session,
                self.current_user.id,
                "complete_global_audit",
                "last open audit have been completed",
                category=AuditLogCategory.audit,
            )

        return self.redirect("/groups/{}".format(audit.group.name))
Пример #43
0
def test_audit_end_to_end(session, users, groups, http_client, base_url,
                          graph):  # noqa: F811
    """ Tests an end-to-end audit cycle. """
    groupname = "audited-team"

    gary_id = users["*****@*****.**"].id

    # make everyone an auditor or global audit will have issues
    add_member(groups["auditors"], users["*****@*****.**"])
    add_member(groups["auditors"], users["*****@*****.**"])
    add_member(groups["auditors"], users["*****@*****.**"])
    add_member(groups["auditors"], users["*****@*****.**"])

    # add some users to test removal
    add_member(groups[groupname], users["*****@*****.**"])
    add_member(groups[groupname], users["*****@*****.**"])

    graph.update_from_db(session)

    # start the audit
    end_at_str = (datetime.now() + timedelta(days=10)).strftime("%m/%d/%Y")
    fe_url = url(base_url, "/audits/create")
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"ends_at": end_at_str}),
        headers={"X-Grouper-User": "******"},
    )
    assert resp.code == 200

    open_audits = get_audits(session, only_open=True).all()
    assert len(open_audits) == 4, "audits created"

    assert groupname in [x.group.name for x in open_audits
                         ], "group we expect also gets audit"

    # pull all the info we need to resolve audits, avoids detached sqlalchemy sessions
    # (DetachedInstanceError)
    MyAuditMemberInfo = NamedTuple("MyAuditMemberInfo", [("am_id", int),
                                                         ("edge_type", int),
                                                         ("edge_id", int)])
    Audit = NamedTuple(
        "Audit",
        [
            ("audit_id", int),
            ("owner_name", str),
            ("group_name", str),
            ("audit_members_infos", List[MyAuditMemberInfo]),
        ],
    )
    all_group_ids = [x.group.id for x in open_audits]
    open_audits = [
        Audit(
            x.id,
            next(iter(x.group.my_owners())),
            x.group.name,
            [
                MyAuditMemberInfo(
                    ami.audit_member_obj.id,
                    ami.audit_member_obj.edge.member_type,
                    ami.audit_member_obj.edge_id,
                ) for ami in get_group_audit_members_infos(session, x.group)
            ],
        ) for x in open_audits
    ]

    # approve everything but the one we added members to
    for one_audit in open_audits:
        fe_url = url(base_url,
                     "/audits/{}/complete".format(one_audit.audit_id))

        if one_audit.group_name == groupname:
            continue

        # blanket approval
        body = urlencode({
            "audit_{}".format(ami.am_id): "approved"
            for ami in one_audit.audit_members_infos
        })

        resp = yield http_client.fetch(
            fe_url,
            method="POST",
            body=body,
            headers={"X-Grouper-User": one_audit.owner_name})
        assert resp.code == 200

    open_audits = get_audits(session, only_open=True).all()
    assert len(open_audits) == 1, "only our test group remaining"

    one_audit = open_audits[0]
    one_audit.id

    body_dict = {}
    for ami in get_group_audit_members_infos(session, one_audit.group):
        if gary_id == ami.member_obj.id:
            # deny
            body_dict["audit_{}".format(ami.audit_member_obj.id)] = "remove"
        else:
            # approve
            body_dict["audit_{}".format(ami.audit_member_obj.id)] = "approved"

    owner_name = next(iter(one_audit.group.my_owners()))
    fe_url = url(base_url, "/audits/{}/complete".format(one_audit.id))
    resp = yield http_client.fetch(fe_url,
                                   method="POST",
                                   body=urlencode(body_dict),
                                   headers={"X-Grouper-User": owner_name})
    assert resp.code == 200

    # check all the logs
    assert len(AuditLog.get_entries(
        session, action="start_audit")) == 1, "global start is logged"
    assert (len(AuditLog.get_entries(
        session,
        action="complete_global_audit")) == 1), "global complete is logged"

    for group_id in all_group_ids:
        assert (len(
            AuditLog.get_entries(
                session,
                on_group_id=group_id,
                action="complete_audit",
                category=AuditLogCategory.audit,
            )) == 1), "complete entry for each group"

    assert (len(
        AuditLog.get_entries(session,
                             on_user_id=gary_id,
                             category=AuditLogCategory.audit)) == 1
            ), "removal AuditLog entry on user"
Пример #44
0
    def post(self, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        if not user_can_manage_group(self.session, group, self.current_user):
            return self.forbidden()

        members = group.my_members()
        my_role = user_role(self.current_user, members)
        form = self.get_form(role=my_role)
        if not form.validate():
            return self.render(
                "group-add.html", form=form, group=group,
                alerts=self.get_form_alerts(form.errors)
            )

        member = get_user_or_group(self.session, form.data["member"])
        if member.type == "User" and is_service_account(self.session, member):
            # For service accounts, we want to always add the group to other groups, not the user
            member = get_service_account(self.session, user=member).group
        if not member:
            form.member.errors.append("User or group not found.")
        elif (member.type, member.name) in group.my_members():
            form.member.errors.append("User or group is already a member of this group.")
        elif group.name == member.name:
            form.member.errors.append("By definition, this group is a member of itself already.")

        # Ensure this doesn't violate auditing constraints
        fail_message = 'This join is denied with this role at this time.'
        try:
            user_can_join = assert_can_join(group, member, role=form.data["role"])
        except UserNotAuditor as e:
            user_can_join = False
            fail_message = e
        if not user_can_join:
            form.member.errors.append(fail_message)

        if form.member.errors:
            return self.render(
                "group-add.html", form=form, group=group,
                alerts=self.get_form_alerts(form.errors)
            )

        expiration = None
        if form.data["expiration"]:
            expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y")

        try:
            group.add_member(
                requester=self.current_user,
                user_or_group=member,
                reason=form.data["reason"],
                status='actioned',
                expiration=expiration,
                role=form.data["role"]
            )
        except InvalidRoleForMember as e:
            return self.render(
                "group-add.html", form=form, group=group,
                alerts=[
                    Alert('danger', e.message)
                ]
            )

        self.session.commit()

        on_user_id = member.id if member.type == "User" else None
        AuditLog.log(self.session, self.current_user.id, 'join_group',
                     '{} added to group with role: {}'.format(
                         member.name, form.data["role"]),
                     on_group_id=group.id, on_user_id=on_user_id)

        if member.type == "User":
            send_email(
                self.session,
                [member.name],
                'Added to group: {}'.format(group.name),
                'request_actioned',
                settings,
                {
                    'group_name': group.name,
                    'actioned_by': self.current_user.name,
                    'reason': form.data['reason'],
                    'expiration': expiration,
                    'role': form.data['role'],
                }
            )

        return self.redirect("/groups/{}?refresh=yes".format(group.name))
Пример #45
0
    def post(self, audit_id):
        user = self.get_current_user()
        if not user_has_permission(self.session, user, PERMISSION_AUDITOR):
            return self.forbidden()

        audit = self.session.query(Audit).filter(Audit.id == audit_id).one()

        # only owners can complete
        owner_ids = {member.id for member in audit.group.my_owners().values()}
        if user.id not in owner_ids:
            return self.forbidden()

        if audit.complete:
            return self.redirect("/groups/{}".format(audit.group.name))

        edges = {}
        for argument in self.request.arguments:
            if argument.startswith('audit_'):
                edges[int(argument.split('_')[1])] = self.request.arguments[argument][0]

        for member in audit.my_members():
            if member.id in edges:
                # You can only approve yourself (otherwise you can remove yourself
                # from the group and leave it ownerless)
                if member.member.id == user.id:
                    member.status = "approved"
                elif edges[member.id] in AUDIT_STATUS_CHOICES:
                    member.status = edges[member.id]

        self.session.commit()

        # Now if it's completable (no pendings) then mark it complete, else redirect them
        # to the group page.
        if not audit.completable:
            return self.redirect('/groups/{}'.format(audit.group.name))

        # Complete audits have to be "enacted" now. This means anybody marked as remove has to
        # be removed from the group now.
        try:
            for member in audit.my_members():
                if member.status == "remove":
                    audit.group.revoke_member(self.current_user, member.member,
                                              "Revoked as part of audit.")
                    AuditLog.log(self.session, self.current_user.id, 'remove_member',
                                 'Removed membership in audit: {}'.format(member.member.name),
                                 on_group_id=audit.group.id, on_user_id=member.member.id,
                                 category=AuditLogCategory.audit)
        except PluginRejectedGroupMembershipUpdate as e:
            alert = Alert("danger", str(e))
            return self.redirect('/groups/{}'.format(audit.group.name), alerts=[alert])

        audit.complete = True
        self.session.commit()

        # Now cancel pending emails
        cancel_async_emails(self.session, 'audit-{}'.format(audit.group.id))

        AuditLog.log(self.session, self.current_user.id, 'complete_audit',
                     'Completed group audit.', on_group_id=audit.group.id,
                     category=AuditLogCategory.audit)

        # check if all audits are complete
        if get_audits(self.session, only_open=True).count() == 0:
            AuditLog.log(self.session, self.current_user.id, 'complete_global_audit',
                    'last open audit have been completed', category=AuditLogCategory.audit)

        return self.redirect('/groups/{}'.format(audit.group.name))
Пример #46
0
    def post(self, request_id, group_id=None, name=None):
        group = Group.get(self.session, group_id, name)
        if not group:
            return self.notfound()

        members = group.my_members()
        my_role = user_role(self.current_user, members)
        if my_role not in ("manager", "owner", "np-owner"):
            return self.forbidden()

        request = self.session.query(Request).filter_by(id=request_id).scalar()
        if not request:
            return self.notfound()

        form = GroupRequestModifyForm(self.request.arguments)
        form.status.choices = self._get_choices(request.status)

        updates = request.my_status_updates()

        if not form.status.choices:
            alerts = (Alert("info", "Request has already been processed"),)
            return self.render(
                "group-request-update.html", group=group, request=request,
                members=members, form=form, alerts=alerts,
                statuses=REQUEST_STATUS_CHOICES, updates=updates
            )

        if not form.validate():
            return self.render(
                "group-request-update.html", group=group, request=request,
                members=members, form=form, alerts=self.get_form_alerts(form.errors),
                statuses=REQUEST_STATUS_CHOICES, updates=updates
            )

        # We have to test this here, too, to ensure that someone can't sneak in with a pending
        # request that used to be allowed.
        if form.data["status"] != "cancelled":
            fail_message = 'This join is denied with this role at this time.'
            try:
                user_can_join = assert_can_join(request.requesting, request.get_on_behalf(),
                                                role=request.edge.role)
            except UserNotAuditor as e:
                user_can_join = False
                fail_message = e
            if not user_can_join:
                return self.render(
                    "group-request-update.html", group=group, request=request,
                    members=members, form=form, statuses=REQUEST_STATUS_CHOICES, updates=updates,
                    alerts=[
                        Alert('danger', fail_message, 'Audit Policy Enforcement')
                    ]
                )

        request.update_status(
            self.current_user,
            form.data["status"],
            form.data["reason"]
        )
        self.session.commit()

        AuditLog.log(self.session, self.current_user.id, 'update_request',
                     'Updated request to status: {}'.format(form.data["status"]),
                     on_group_id=group.id, on_user_id=request.requester.id)

        edge = self.session.query(GroupEdge).filter_by(
            id=request.edge_id
        ).one()

        approver_mail_to = [
            user.name
            for user in group.my_approver_users()
            if user.name != self.current_user.name and user.name != request.requester.username
        ]

        subj = "Re: " + self.render_template(
            'email/pending_request_subj.tmpl',
            group=group.name,
            user=request.requester.username
        )

        send_email(
            self.session,
            approver_mail_to,
            subj,
            "approver_request_updated",
            settings,
            {
                'group_name': group.name,
                'requester': request.requester.username,
                'changed_by': self.current_user.name,
                'status': form.data['status'],
                'role': edge.role,
                'reason': form.data['reason'],
                'references_header': request.reference_id,
            },
        )

        if form.data['status'] == 'actioned':
            send_email(
                self.session,
                [request.requester.name],
                'Added to group: {}'.format(group.groupname),
                'request_actioned',
                settings,
                {
                    'group_name': group.name,
                    'actioned_by': self.current_user.name,
                    'reason': form.data['reason'],
                    'expiration': edge.expiration,
                    'role': edge.role,
                }
            )
        elif form.data['status'] == 'cancelled':
            send_email(
                self.session,
                [request.requester.name],
                'Request to join cancelled: {}'.format(group.groupname),
                'request_cancelled',
                settings,
                {
                    'group_name': group.name,
                    'cancelled_by': self.current_user.name,
                    'reason': form.data['reason'],
                    'expiration': edge.expiration,
                    'role': edge.role,
                }
            )

        # No explicit refresh because handler queries SQL.
        if form.data['redirect_aggregate']:
            return self.redirect("/user/requests")
        else:
            return self.redirect("/groups/{}/requests".format(group.name))