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
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)
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
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 )
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'}
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)
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'
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)
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)
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.')
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)
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
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
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
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
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)
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))
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', }
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', }
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')
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', ]
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
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()
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)
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, )
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
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})
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'}