Beispiel #1
0
    def set_user_role(self, role, uid=None, user_name=None):
        # Make sure the role is valid
        if not Perms.is_valid_role(role):
            raise ValidationError("%server_error_invalid_role")

        with Transaction(self.db):
            # Read the user data
            user = self.db.get_user(uid=uid, user_name=user_name)
            if user is None:
                raise NotFoundError()

            # Update the role
            if role != user.role:
                # Delete any active session, if the user has fewer permissions
                perms_old = Perms.lookup_role_permissions(user.role)
                perms_new = Perms.lookup_role_permissions(role)
                if perms_old & (perms_new ^ perms_old):
                    # Note: (perms_new ^ perms_old) contains the changed
                    # permission bits, perms_old & (perms_new ^ perms_old)
                    # is not equal to zero if a bit that changed was active in
                    # perms_old.
                    self.db.purge_sessions_for_user(user.uid)

                # Write the user back with the updated role
                user.role = role
                self.db.update_user(user)
Beispiel #2
0
    def update_user_settings(self, uid, settings):
        """
        Merges the currently stored settings with the given settings object.
        Returns the updated settings object.
        """

        # TODO: Provide method to not update the settings, as currently an
        # ill-behaving client has no chance to remove setting keys.

        # Make sure the "settings" object is a dictionary
        if not isinstance(settings, dict):
            raise ValidationError()

        # Perform the update
        with Transaction(self.db):
            # Merge the new settings with the old values
            if (uid in self.db.settings):
                for key, value in json.loads(self.db.settings[uid]).items():
                    if not key in settings:
                        settings[key] = value

            # Serialise the settings to a string
            settings_str = json.dumps(settings, sort_keys=True)
            if len(settings_str) > API.MAX_SETTINGS_LEN:
                raise ValidationError()

            # Store the merged values in the database
            self.db.settings[uid] = settings_str
            return settings
Beispiel #3
0
    def create_post(self, post):
        """
        Creates a new post.
        """

        # Coerce the post into a new Post object, make sure the pid is set to
        # "None"; otherwise the post would not get its own pid
        p = API.coerce_post(post)
        if not p.pid is None:
            raise ValidationError()

        # The revision must be set to zero
        if p.revision != 0:
            raise ValidationError()

        # Insert the post into the database
        with Transaction(self.db):
            # Create the post in the database
            p.pid = self.db.create_post(p)

            # Update the fulltext search
            self.db.update_fulltext(p.pid, p)

            # Insert keywords
            keywords = self.db.keywords
            for keyword in API._split(p.keywords):
                keywords[keyword] = p.pid

            # Convert the post back to a dictionary
            return API._post_to_dict(p)
Beispiel #4
0
    def import_from_object(self, obj):
        """
        Restors a database backup formerly created by the export_to_json
        function. This will delete the current content of the database.

        @param obj is a Python object that has been deserialised from JSON
        """

        # Make sure that this operation is atomic
        with Transaction(self.db):
            # Delete everything in the database
            self.db.purge()

            # Go through all restorable tables (i.e., it doesn't make much sense
            # to backup the challenges, sessions, cache, keywords tables).
            if "configuration" in obj:
                for key, value in obj["configuration"].items():
                    self.db.configuration[key] = value
            if "posts" in obj:
                for post in obj["posts"]:
                    p = API.coerce_post(post)
                    self.db.create_post(p)
                    self.db.update_fulltext(p.pid, p)
            if "posts_history" in obj:
                for post in obj["posts_history"]:
                    self.db.create_post(API.coerce_post(post), history=True)
            if "users" in obj:
                for user in obj["users"]:
                    self.db.create_user(API.coerce_user(user))
            if "settings" in obj:
                for key, value in obj["settings"].items():
                    self.db.settings[key] = value

            # Rebuild the keywords table
            self._rebuild_keywords()
Beispiel #5
0
 def get_user_settings(self, uid):
     """
     Reads the user settings from the database and serialises them into a
     JSON object. Returns an empty object if the user has not stored any
     settings yet.
     """
     with Transaction(self.db):
         if (uid in self.db.settings):
             return json.loads(self.db.settings[uid])
         return {}
Beispiel #6
0
    def update_post(self, post):
        """
        Updates an already existing post.
        """

        # Coerce the given post into a Post object, make sure the pid is set
        p = API.coerce_post(post)
        if p.pid is None:
            raise ValidationError()

        with Transaction(self.db):
            # Fetch the post that is supposed to be updated
            pid = p.pid
            old_post = self.db.get_post(pid)
            if old_post is None:
                raise NotFoundError()

            # Do nothing if there is no difference between the old and new post
            if ((old_post.author == p.author) and (old_post.date == p.date)
                    and (old_post.keywords == p.keywords)
                    and (old_post.content == p.content)):
                return API._post_to_dict(p)

            # Make sure that the revision of the given post is equal to the
            # revision of the old post; increment the revision of the post that
            # is to be stored.
            if old_post.revision != p.revision:
                raise ConflictError()
            p.revision += 1

            # Copy the original creation uid and time
            p.cuid = old_post.cuid
            p.ctime = old_post.ctime

            # Insert the old post into the history table
            self.db.create_post(old_post, history=True)

            # Remove keywords associated with the old post
            keywords = self.db.keywords
            for keyword in API._split(old_post.keywords):
                keywords[keyword] = keywords[keyword] - set((pid, ))

            # Update the post in the normal posts table
            if self.db.update_post(p) == 0:
                raise NotFoundError()

            # Update the fulltext search
            self.db.update_fulltext(pid, p)

            # Insert the new keywords
            keywords = self.db.keywords
            for keyword in API._split(p.keywords):
                keywords[keyword] = pid

            return API._post_to_dict(p)
Beispiel #7
0
 def get_configuration_object(self):
     """
     Returns a JSON serialisable object containing the global configuration
     options.
     """
     with Transaction(self.db):
         c = self.db.configuration
         return {
             "login_methods": {
                 "username_password":
                 c["login_method_username_password"] == "1",
                 "cas": c["login_method_cas"] == "1",
             },
             "salt": c["salt"],
         }
Beispiel #8
0
    def _login_method_username_password(self, user_name, challenge, response):
        """
        Internal implementation of the _login_method_username_password function.
        """

        # Make sure the user name, challenge, and response are of the
        # correct format
        user_name = str(user_name).strip().lower()
        challenge, response = str(challenge), str(response)
        if not (API.USER_RE.match(user_name) and API.HEX64_RE.match(challenge)
                and API.HEX64_RE.match(response)):
            raise ValidationError()

        # Make sure the valid is valid (do this outside of the transaction
        # below to avoid purged sessions being rolled back.
        if not self._check_password_login_challenge(challenge):
            raise AuthentificationError()

        # Lookup the user, check the user role and compute the expected response
        with Transaction(self.db):
            # Make sure the user exists. If the user name does not exist, raise
            # an authentification error, i.e. do not expose the users'
            # non-existance to the client
            user = self.db.get_user_by_name(user_name)
            if user is None:
                raise AuthentificationError()

            # Make sure the user can actually login using a password
            if user.auth_method != "password":
                raise AuthentificationError()

            # Make sure the user has the right permissions
            if Perms.lookup_role_permissions(user.role) <= 0:
                raise AuthentificationError()

            # Compute the expected password hash
            expected_response = self._hash_password(
                bytes.fromhex(user.password), challenge)
            if expected_response != response:
                raise AuthentificationError()

            # Okay, all this worked. Let's create a session for the user and
            # return the sid
            sid = os.urandom(32).hex()
            self.db.create_session(sid, user.uid)
            return self.get_session_data(sid)
Beispiel #9
0
    def export_to_object(self, export_passwords=False):
        """
        Exports the entire database into a JSON serialisable object. Some tables
        with volatile data (such as the "cache", "challenges", and "sessions"
        tables) will not be exported.
        """
        with Transaction(self.db):
            # Export the configuration options
            config_obj = {}
            for key, value in self.db.configuration.items():
                config_obj[key] = value

            # Export the posts
            posts_arr = []
            for post in self.db.list_posts():
                posts_arr.append(asdict(post))

            # Export the post history
            posts_history_arr = []
            for post in self.db.list_posts(history=True):
                posts_history_arr.append(asdict(post))

            # Export the users
            users_arr = []
            for user in self.db.list_users():
                user_obj = asdict(user)
                if not export_passwords:
                    user_obj["reset_password"] = True
                    del user_obj["password"]
                users_arr.append(user_obj)

            # Export the user settings
            settings_obj = {}
            for key, value in self.db.settings.items():
                settings_obj[key] = value

            # Return the completely assembled object
            return {
                "configuration": config_obj,
                "posts": posts_arr,
                "posts_history": posts_history_arr,
                "users": users_arr,
                "settings": settings_obj
            }
Beispiel #10
0
    def get_session_data(self, sid):
        """
        Returns the session data. Returns None if the given session is not
        available or invalid. Updates the session mtime in order to re-validate
        the session. The session data is an object containing the following elements:

            {
                "sid": <the session id>,
                "uid": <the numerical user id>,
                "name": <the canonical user name>,
                "display_name": <the user-defined display name>,
                "role": <the user role string>,
                "reset_password": <true if the user must reset the password>,
            }

        """

        # Validate the session identifier
        if not API.HEX64_RE.match(sid):
            raise ValidationError()

        # Remove any stale sessions, then try to fetch the user data associated
        # with the session
        self.db.purge_stale_sessions(API.SESSION_TIMEOUT)
        with Transaction(self.db):
            # Fetch the UID corresponding to the session
            uid = self.db.get_session_uid(sid)
            if not uid:
                return None

            # Advance the session mtime
            self.db.update_session_mtime(sid)

            # Get the user data associated with the UID
            user = self.db.get_user_by_id(uid)
            return {
                "sid": sid,
                "uid": uid,
                "name": user.name,
                "display_name": user.display_name,
                "role": user.role,
                "reset_password": user.reset_password,
            }
Beispiel #11
0
    def _check_password_login_challenge(self, challenge):
        """
        Used internally. Returns True and deletes the given challenge if it
        existed, returns
        False if the challenge is invalid (i.e. too old).
        """

        with Transaction(self.db):
            # Delete stale challenges
            self.db.purge_stale_challenges(API.CHALLENGE_TIMEOUT)

            # Check whether the callenge exists
            if not challenge in self.db.challenges:
                return False

            # Delete the challenge
            del self.db.challenges[challenge]

            return True
Beispiel #12
0
    def reset_user_password(self, uid=None, user_name=None):
        with Transaction(self.db):
            user = self.db.get_user(uid=uid, user_name=user_name)
            if user is None:
                raise NotFoundError()

            # Create a random password
            password = self.create_random_password()
            password_hash = self._hash_password(password)

            # Set the password and the reset_password flag
            user.password = password_hash
            user.reset_password = True

            # Write the user back to the database
            self.db.update_user(user)

            # Return the generated password for display
            return password
Beispiel #13
0
    def get_password_login_challenge(self):
        """
        Creates a login challenge and returns an object containing both the
        challenge and the salt.
        """

        with Transaction(self.db):
            # Fetch the current UNIX time and create a random challenge
            challenge = os.urandom(32).hex()

            # Delete old challenges
            self.db.purge_stale_challenges(API.CHALLENGE_TIMEOUT)

            # Store the challenge and its creation time in the database
            self.db.challenges[challenge] = self.db.now()

            # Return a challenge object ready to be sent to the client
            return {
                "salt": self.db.configuration["salt"],
                "challenge": challenge
            }
Beispiel #14
0
    def delete_post(self, pid):
        # Make sure the given post id is and integer and non-negative
        if not isinstance(pid, int) or pid < 0:
            raise ValidationError()

        with Transaction(self.db):
            # Fetch the post that is supposed to be deleted
            post = self.db.get_post(pid)
            if post is None:
                raise NotFoundError()

            # Delete all keywords associated with the post
            keywords = self.db.keywords
            for keyword in API._split(post.keywords):
                keywords[keyword] = keywords[keyword] - set((pid, ))

            # Delete the post from the history, the main post table, and the
            # fulltext search index
            self.db.delete_post(pid)
            self.db.delete_post(pid, history=True)
            self.db.delete_fulltext(pid)
Beispiel #15
0
    def update_user(self, properties, uid=None, user_name=None):
        """
        Updates attributes of a user by merging the given properties into an
        existing user structure.
        """

        with Transaction(self.db):
            # Try to fetch the user, either by user name or uid
            user = self.db.get_user(uid=uid, user_name=user_name)
            if user is None:
                raise NotFoundError()

            # Make sure the settings we're trying to update actually exist, and,
            # in case they do, update the corresponding value
            for key, value in properties.items():
                # If we're updating the password to a new password and the new
                # password is actually different from the current password,
                # then reset the "reset_password" flag
                if ((key == "password")
                        and (value.lower() != user.password.lower())):
                    user.reset_password = False

                # Update all other properties
                if hasattr(user, key):
                    setattr(user, key, value)
                else:
                    raise ValidationError()

            # Coerce the updated user to make sure all new settings adhere to
            # the rules
            user_new = API.coerce_user(asdict(user))
            try:
                self.db.update_user(user_new)
            except UniqueKeyViolationError:
                raise ConflictError()

            return asdict(user_new)
Beispiel #16
0
    def _rebuild_keywords(self):
        """
        Given the current list of posts, rebuilds the index that maps keywords
        onto a set of posts. This function is used after importing a backup.
        """
        with Transaction(self.db):
            # Delete all keywords
            self.db.keywords.clear()

            # List all posts and construct a mapping between posts and keywords
            keywords = {}
            for post in self.db.list_posts():
                for keyword in API._split(post.keywords):
                    if keyword in keywords:
                        keywords[keyword].add(post.pid)
                    else:
                        keywords[keyword] = {
                            post.pid,
                        }

            # Insert the keywords into the database; the keywords dictionary
            # is a MultiDict, i.e., it stores a set per keyword
            for keyword, pids in keywords.items():
                self.db.keywords[keyword] = pids
Beispiel #17
0
    def delete_user(self, uid=None, user_name=None, force=False):
        """
        Deletes the user with the given uid or user name. Per default, does not
        delete users who already have posts; to this end, "force" has to be set
        to True. Important: this function will move all content generated by
        this user to the dummy "[deleted]" user account.

        @param uid is the numerical id of the user that should be deleted.
               Supply either uid or user_name.
        @param user_name is the name of the user that should be deleted. Supply
               either uid or user_name.
        @param force if true, force the deletion of users who contributed
               content.
        """

        with Transaction(self.db):
            # Read the user data
            user = self.db.get_user(uid=uid, user_name=user_name)
            if user is None:
                raise NotFoundError()

            # Check whether the user has any posts. If yes, either rewrite these
            # posts to the "[deleted]" user (if force=True) or do nothing and
            # abort (if force=False).
            def _rewrite(history):
                # List all the posts for the user
                filter = FilterUID(user.uid) | FilterAuthor(user.uid)
                user_posts = self.db.list_posts(history=history, filter=filter)

                # Abort if we found any posts, and "force" is not set
                if len(user_posts) > 0 and not force:
                    return False

                # Otherwise, update the post CUID or MUID to "0" and update the
                # post
                for post in user_posts:
                    if post.cuid == user.uid:
                        post.cuid = 0
                    if post.muid == user.uid:
                        post.muid = 0
                    if post.author == user.uid:
                        post.author = 0
                    self.db.update_post(post, history=history)

                return True

            # Rewrite both the current post table and the history table
            if not (_rewrite(history=False) and _rewrite(history=True)):
                return False

            # Delete all sessions for this user
            self.db.purge_sessions_for_user(user.uid)

            # Delete all settings for this user
            if user.uid in self.db.settings:
                del self.db.settings[user.uid]

            # Delete the user account itself
            self.db.delete_user(user.uid)

            # Success!
            return True
Beispiel #18
0
 def _init(self):
     if self.db.open and self.perform_initialisation:
         with Transaction(self.db):
             self._init_configuration()
             self._init_users()
         self.perform_initialisation = False