Exemple #1
0
 def new(
     cls,
     article_id: int,
     title: str,
     language_id: int,
     contents: str,
     user_id: int,
 ) -> 'WikiArticle':
     User.is_valid(user_id, error=True)
     cache.delete(
         cls.__cache_key_from_article__.format(article_id=article_id)
     )
     translation = super()._new(
         article_id=article_id,
         language_id=language_id,
         title=title,
         contents=contents,
     )
     if WikiAlias.is_valid(title):
         WikiAlias.new(alias=title, article_id=article_id)
     WikiRevision.new(
         article_id=article_id,
         language_id=language_id,
         title=title,
         editor_id=user_id,
         contents=contents,
     )
     return translation
Exemple #2
0
 def new(
     cls, *, user_id: int, thread_id: int
 ) -> Optional['ForumThreadSubscription']:
     ForumThread.is_valid(thread_id, error=True)
     User.is_valid(user_id, error=True)
     cache.delete(cls.__cache_key_users__.format(thread_id=thread_id))
     cache.delete(cls.__cache_key_of_user__.format(user_id=user_id))
     return super()._new(user_id=user_id, thread_id=thread_id)
Exemple #3
0
def test_from_cache(app, authed_client):
    """Get a user from the cache."""
    user = User.from_pk(1)
    cache.cache_model(user, timeout=60)
    user_new = User.from_cache('users_1')
    assert user_new.id == 1
    assert user_new.email == '*****@*****.**'
    assert user_new.enabled is True
    assert user_new.inviter_id is None
Exemple #4
0
 def new(
     cls, *, post_id: int, editor_id: int, contents: str, time: datetime
 ) -> Optional[ForumPost]:
     ForumPost.is_valid(post_id, error=True)
     User.is_valid(editor_id, error=True)
     cache.delete(cls.__cache_key_of_post__.format(id=post_id))
     return super()._new(
         post_id=post_id, editor_id=editor_id, contents=contents, time=time
     )
Exemple #5
0
def test_user_permissions_property_cached(app, client, monkeypatch):
    """Permissions property should properly handle differences in userclasses and custom perms."""
    add_permissions(app, 'one', 'three', 'four')
    user = User.from_pk(1)
    assert set(user.permissions) == {'four', 'one', 'three'}
    assert cache.has(user.__cache_key_permissions__.format(id=user.id))
    del user
    with mock.patch('core.users.models.User.user_class_model', None):
        user = User.from_pk(1)
        assert set(user.permissions) == {'four', 'one', 'three'}
Exemple #6
0
 def new(cls, conv_id: int, user_id: int,
         contents: str) -> Optional['PrivateMessage']:
     """
     Create a message in a PM conversation.
     """
     PrivateConversation.is_valid(conv_id, error=True)
     User.is_valid(user_id, error=True)
     PrivateConversationState.update_last_response_time(conv_id, user_id)
     return super()._new(conv_id=conv_id,
                         user_id=user_id,
                         contents=contents)
Exemple #7
0
def test_get_from_cache(app, authed_client):
    """Test that cache values are used instead of querying a user."""
    add_permissions(app, 'users_view')
    user = User.from_pk(1)
    data = {}
    for attr in user.__table__.columns.keys():
        data[attr] = getattr(user, attr, None)
    data['username'] = '******'
    cache.set(user.cache_key, data)
    user = User.from_pk(1)
    assert user.username == 'fakeshit'
Exemple #8
0
def test_cache_autoclear_dirty_and_deleted(app, client):
    """The cache autoclears dirty models upon commit."""
    user = User.from_pk(1)
    user_2 = User.from_pk(3)
    user.set_password('testing')
    db.session.delete(user_2)
    assert cache.has('users_1')
    assert cache.has('users_3')
    db.session.commit()
    assert not cache.has('users_1')
    assert not cache.has('users_3')
    assert not User.from_pk(3)
Exemple #9
0
 def new(cls, user_id: int, type: str,
         contents: Dict[str, Union[Dict, str]]) -> 'Notification':
     User.is_valid(user_id, error=True)
     noti_type = NotificationType.from_type(type, create_new=True)
     cache.delete(
         cls.__cache_key_of_user__.format(user_id=user_id, type=type))
     cache.delete(
         cls.__cache_key_notification_count__.format(user_id=user_id,
                                                     type=type))
     return super()._new(user_id=user_id,
                         type_id=noti_type.id,
                         contents=contents)
Exemple #10
0
def users_edit_settings(
    user: User, existing_password: str = None, new_password: str = None
) -> flask.Response:
    """
    Change a user's settings. Requires the ``users_edit_settings`` permission.
    Requires the ``users_moderate`` permission to change another user's
    settings, which can be done by specifying a ``user_id``.

    .. :quickref: Settings; Change settings.

    **Example request**:

    .. parsed-literal::

       PUT /users/settings HTTP/1.1

       {
         "existing_password": "******",
         "new_password": "******"
       }

    **Example response**:

    .. parsed-literal::

       {
         "status": "success",
         "response": "Settings updated."
       }

    :json string existing_password: User's existing password, not needed
        if setting another user's password with ``moderate_user`` permission.
    :json string new_password: User's new password. Must be 12+ characters and contain
        at least one letter, one number, and one special character.

    :statuscode 200: Settings successfully updated
    :statuscode 400: Settings unsuccessfully updated
    :statuscode 403: User does not have permission to change user's settings
    """
    if new_password:
        if not flask.g.user.has_permission(UserPermissions.CHANGE_PASS):
            raise _403Exception(
                message='You do not have permission to change this password.'
            )
        if not existing_password or not user.check_password(existing_password):
            raise _401Exception(message='Invalid existing password.')
        user.set_password(new_password)
        APIKey.update_many(
            pks=APIKey.hashes_from_user(user.id), update={'revoked': True}
        )

    db.session.commit()
    return flask.jsonify('Settings updated.')
Exemple #11
0
    def new(
        cls, *, poll_id: int, user_id: int, choice_id: bool
    ) -> 'ForumPollAnswer':
        User.is_valid(user_id, error=True)
        # ForumPollChoice also validates ForumPoll.
        ForumPollChoice.is_valid_choice(choice_id, poll_id=poll_id, error=True)
        if cls.from_attrs(poll_id=poll_id, user_id=user_id):
            raise APIException('You have already voted for this poll.')

        cache.delete(
            ForumPollChoice.__cache_key_answers__.format(id=choice_id)
        )
        return cls._new(poll_id=poll_id, user_id=user_id, choice_id=choice_id)
Exemple #12
0
 def new(
     cls, *, thread_id: int, user_id: int, contents: str
 ) -> Optional['ForumPost']:
     ForumThread.is_valid(thread_id, error=True)
     User.is_valid(user_id, error=True)
     cache.delete(cls.__cache_key_of_thread__.format(id=thread_id))
     post = super()._new(
         thread_id=thread_id, user_id=user_id, contents=contents
     )
     send_subscription_notices(post)
     check_post_contents_for_quotes(post)
     check_post_contents_for_mentions(post)
     return post
Exemple #13
0
 def new(cls, title: str, contents: str, user_id: int) -> 'WikiArticle':
     User.is_valid(user_id, error=True)
     WikiAlias.is_valid(title, error=True)
     cache.delete(cls.__cache_key_all__)
     article = super()._new(title=title, contents=contents)
     WikiAlias.new(alias=title, article_id=article.id)
     WikiRevision.new(
         article_id=article.id,
         language_id=1,
         title=title,
         editor_id=user_id,
         contents=contents,
     )
     return article
Exemple #14
0
 def new(
     cls, topic: str, forum_id: int, creator_id: int, post_contents: str
 ) -> Optional['ForumThread']:
     Forum.is_valid(forum_id, error=True)
     User.is_valid(creator_id, error=True)
     cache.delete(cls.__cache_key_of_forum__.format(id=forum_id))
     thread = super()._new(
         topic=topic, forum_id=forum_id, creator_id=creator_id
     )
     subscribe_users_to_new_thread(thread)
     ForumPost.new(
         thread_id=thread.id, user_id=creator_id, contents=post_contents
     )
     return thread
Exemple #15
0
def check_api_key() -> None:
    """
    Checks the request header for an authorization key and, if the key matches
    an active API key, sets the flask.g.user and flask.g.api_key context globals.
    """
    raw_key = parse_key(flask.request.headers)
    if raw_key and len(raw_key) > 10:
        # The API Key stores the identification hash as the first 10 values,
        # and the secret after it, so the key can be looked up and then
        # compared with the hash function.
        api_key = APIKey.from_pk(raw_key[:10])  # Implied active_only
        if api_key and api_key.check_key(raw_key[10:]) and not api_key.revoked:
            if not api_key.permanent:
                time_since = (
                    datetime.utcnow().replace(tzinfo=pytz.utc)
                    - api_key.last_used
                )
                if time_since.total_seconds() > api_key.timeout:
                    api_key.revoked = True
                    db.session.commit()
                    raise _401Exception
            flask.g.user = User.from_pk(api_key.user_id)
            flask.g.api_key = api_key
            if flask.g.user.has_permission(SitePermissions.NO_IP_HISTORY):
                flask.request.environ['REMOTE_ADDR'] = '0.0.0.0'
            update_api_key(api_key)
        else:
            raise _401Exception
Exemple #16
0
def delete_members(id: int, user_ids: List[int]):
    conv = PrivateConversation.from_pk(id,
                                       _404=True,
                                       asrt=MessagePermissions.VIEW_OTHERS)
    not_members = [
        str(uid) for uid in user_ids
        if uid not in {u.id
                       for u in conv.members}
    ]
    if not_members:
        raise APIException(
            'The following user_ids are not in the conversation: '  # type: ignore
            f'{", ".join(not_members)}.')

    states = []
    og_members = []
    for uid in list(set(user_ids)):
        st = PrivateConversationState.from_attrs(conv_id=id, user_id=uid)
        states.append(st)
        if st.original_member:
            og_members.append(User.from_pk(st.user_id).username)
    if og_members:
        raise APIException(
            'The following original members cannot be removed from the conversation: '
            f'{", ".join(og_members)}.')
    for st in states:
        st.deleted = True
    db.session.commit()
    conv.del_property_cache('members')
    cache.delete(
        PrivateConversationState.__cache_key_members__.format(conv_id=conv.id))
    return flask.jsonify(conv.members)
Exemple #17
0
def get_user(user_id: int) -> flask.Response:
    """
    Return general information about a user with the given user ID.  If the
    user is getting information about themselves, the API will return more
    detailed data about the user. If the requester has the
    ``users_moderate`` permission, the API will return *even more* data.

    .. :quickref: User; Get user information.

    **Example response**:

    .. parsed-literal::

       HTTP/1.1 200 OK
       Vary: Accept
       Content-Type: application/json

       {
         "status": "success",
         "response": "<User>"
       }

    :statuscode 200: User exists
    :statuscode 404: User does not exist
    """
    return flask.jsonify(User.from_pk(user_id, _404=True))
Exemple #18
0
def test_serialize_very_detailed(app, authed_client):
    add_permissions(app, 'users_moderate', 'users_moderate_advanced')
    user = User.from_pk(1)
    data = NewJSONEncoder().default(user)
    check_dictionary(
        data,
        {
            'id': 1,
            'username': '******',
            'email': '*****@*****.**',
            'enabled': True,
            'locked': False,
            'user_class': 'User',
            'secondary_classes': ['FLS'],
            'uploaded': 5368709120,
            'downloaded': 0,
            'invites': 1,
            'inviter': None,
            'basic_permissions': [],
        },
    )
    assert 'api_keys' in data and len(data['api_keys']) == 2
    assert 'permissions' in data and set(data['permissions']) == {
        'users_moderate',
        'users_moderate_advanced',
    }
Exemple #19
0
def test_locked_account_permissions(app, client):
    user = User.from_pk(1)
    user.locked = True
    assert set(user.permissions) == {
        'view_staff_pm',
        'send_staff_pm',
        'resolve_staff_pm',
    }
Exemple #20
0
def test_cache_doesnt_autoclear_dirty_and_deleted_(app, client, monkeypatch):
    """The cache does not autoclear dirty models upon commit if they do not have cache keys."""
    user = User.from_pk(1)
    monkeypatch.setattr('core.users.models.User.__cache_key__', None)
    user.set_password('testing')
    assert cache.has('users_1')
    db.session.commit()
    assert cache.has('users_1')
Exemple #21
0
def modify_core():
    User.assign_attrs(
        __cache_key_forum_post_count__='users_{id}_forum_post_count',
        __cache_key_forum_thread_count__='users_{id}_forum_thread_count',
        forum_thread_count=forum_thread_count,
        forum_post_count=forum_post_count,
        forum_permissions=forum_permissions,
    )
    UserSerializer.assign_attrs(
        forum_permissions=Attribute(permission='users_moderate', nested=False))
    Permissions.permission_regexes['basic'] += [
        re.compile('forumaccess_forum_\d+$'),
        re.compile('forumaccess_thread_\d+$'),
    ]
    Config.BASIC_PERMISSIONS += [
        'forums_posts_create',
        'forums_threads_create',
    ]
Exemple #22
0
def test_cache_model(app, authed_client):
    """Test that caching a model works."""
    user = User.from_pk(1)
    cache.cache_model(user, timeout=60)
    user_data = cache.get('users_1')
    assert user_data['id'] == 1
    assert user_data['username'] == 'user_one'
    assert user_data['enabled'] is True
    assert user_data['inviter_id'] is None
Exemple #23
0
def authed_client(app, monkeypatch):
    monkeypatch.setattr(app, 'before_request_funcs', {})
    with set_globals(app):
        with app.app_context():
            user = User.from_pk(1)
    with set_globals(app):
        with set_user(app, user):
            with app.app_context():
                db.session.add(user)
                yield app.test_client()
Exemple #24
0
def test_revoke_invite(app, authed_client, code, expected, invites):
    """Revoking an invite should work only for active invites."""
    add_permissions(app, 'invites_revoke')
    response = authed_client.delete(f'/invites/{code}')
    user = User.from_pk(1)
    check_json_response(response, expected)
    assert user.invites == invites
    if 'expired' in expected and expected['expired'] is True:
        invite = Invite.from_pk(code, include_dead=True)
        assert invite.expired is True
def test_moderate_user_incomplete(app, authed_client):
    add_permissions(app, 'users_moderate')
    response = authed_client.put(
        '/users/2', data=json.dumps({'password': '******'})
    )
    check_json_response(
        response, {'id': 2, 'email': '*****@*****.**', 'downloaded': 0}
    )
    user = User.from_pk(2)
    assert user.check_password('abcdefGHIfJK12#')
    assert user.email == '*****@*****.**'
def test_check_permission_error(app, authed_client, permissions, error):
    add_permissions(app, 'sample_one', 'sample_two')
    db.engine.execute(
        """INSERT INTO users_permissions (user_id, permission, granted)
                      VALUES (1, 'sample_three', 'f')""")
    db.engine.execute("""UPDATE user_classes
                      SET permissions = '{"sample_four", "sample_five"}'
                      WHERE name = 'User'""")
    with pytest.raises(APIException) as e:
        check_permissions(User.from_pk(1), permissions)
    assert all(w in e.value.message for w in error)
Exemple #27
0
 def new(
     cls,
     conv_id: int,
     user_id: int,
     original_member: bool = False,
     read: bool = False,
 ) -> Optional['PrivateConversationState']:
     """
     Create a private message object, set states for the sender and receiver,
     and create the initial message.
     """
     PrivateConversation.is_valid(conv_id, error=True)
     User.is_valid(user_id, error=True)
     cache.delete(cls.__cache_key_members__.format(conv_id=conv_id))
     return super()._new(
         conv_id=conv_id,
         user_id=user_id,
         original_member=original_member,
         read=read,
     )
Exemple #28
0
def test_revoke_invite_others(app, authed_client):
    """
    Reovoking another's invite with the proper permissions should work and re-add
    the invite to the inviter's invite count.
    """
    add_permissions(
        app, 'invites_revoke', 'invites_revoke_others', 'invites_view_others'
    )
    response = authed_client.delete(f'/invites/{CODE_3}')
    check_json_response(response, {'expired': True})
    user = User.from_pk(2)
    assert user.invites == 1
Exemple #29
0
def register(username: str,
             password: str,
             email: str,
             code: str = None) -> flask.Response:
    """
    Creates a user account with the provided credentials.
    An invite code may be required for registration.

    .. :quickref: User; Register a new user.

    **Example request**:

    .. parsed-literal::

       POST /register HTTP/1.1

       {
         "username": "******",
         "password": "******",
         "email": "*****@*****.**",
         "code": "my-invite-code"
       }

    **Example response**:

    .. parsed-literal::

       {
         "status": "success",
         "response": {
           "username": "******"
         }
       }

    :json username: Desired username: must start with an alphanumeric
        character and can only contain alphanumeric characters,
        underscores, hyphens, and periods.
    :json password: Desired password: must be 12+ characters and contain
        at least one letter, one number, and one special character.
    :json email: A valid email address to receive the confirmation email,
        as well as account or security related emails in the future.
    :json code: (Optional) An invite code from another member. Required
        for registration if the site is invite only, otherwise ignored.

    :>jsonarr string username: username the user signed up with

    :statuscode 200: registration successful
    :statuscode 400: registration unsuccessful
    """
    ValInviteCode(code)
    user = User.new(username=username, password=password, email=email)
    return flask.jsonify({'username': user.username})
Exemple #30
0
def test_user_permissions_property(app, client):
    """Permissions property should properly handle differences in userclasses and custom perms."""
    add_permissions(app, 'one', 'three', 'four')
    db.session.execute("""UPDATE user_classes
                       SET permissions = '{"one", "five", "six", "four"}'""")
    db.session.execute(
        """UPDATE secondary_classes SET permissions = '{"five", "two", "one"}'"""
    )
    db.session.execute(
        """INSERT INTO users_permissions (user_id, permission, granted)
                       VALUES (1, 'six', 'f')""")
    user = User.from_pk(1)
    assert set(user.permissions) == {'four', 'one', 'two', 'five', 'three'}