Пример #1
0
    def wrapper(*args, **kwargs):
        # Allow unit tests to skip authentication
        if "testing" in config.config and config.config["testing"]:
            return func(*args, **kwargs, user="******")

        if "Authorization" not in request.headers:
            return response("false", "Authorization required", True), 403
        try:
            decoded = base64.standard_b64decode(
                request.headers["Authorization"].split(" ")[1]).decode("utf-8")
            access_token, firebase_token = decoded.split(":")
            uid = auth.verify_id_token(firebase_token)["uid"]
        except Exception as e:
            log.info("User failed authentication: {}".format(e))
            return response("false", "Failed to authenticate: {}".format(e),
                            True), 403

        engine = database.get_engine()
        query = sql.select([database.auth]).where(
            and_(database.auth.c.user_id == uid,
                 database.auth.c.access_token == access_token))
        results = engine.execute(query)
        if len(next(results, {})) == 0:
            return response("false",
                            "Authorization failed, please enroll first",
                            True), 403

        log.debug("Authenticated user \"{}\"".format(uid))
        return func(*args, **kwargs, user=uid)
Пример #2
0
def process_media(b64, mime_type, user):
    """Process a media file from base64, validating its mime type in the process."""
    data = base64.urlsafe_b64decode(b64 + ("=" * (4 - len(b64) % 4)))
    mime = magic.Magic(mime=True)
    file_type = mime.from_buffer(data)
    if file_type.find(mime_type) == -1:
        log.warning(
            "User {} attempted to upload file with wrong MIME type, {}".format(
                user, file_type))
        return response(
            "false", "Received file but was of type {} instead of {}/*".format(
                file_type, mime_type), True), 400

    # Save image file to UUID-based name, with an extension determined by the file's MIME type
    # Using a UUID allows us to guarantee unique filenames while embedding user and time information
    # The node computation takes the SHA-1 of the username (similar to the original Node server) and
    # uses it as the node value mod 2^48 (node values are 48 bits). This lets us embed user info in
    # the filename, but not in a back-traceable way.
    node = None if user is None else int(
        hashlib.sha1(user.encode("utf-8")).hexdigest(), 16) % (1 << 48)
    file = "{uuid}.{ext}".format(uuid=uuid.uuid1(node=node),
                                 ext=file_type.split("/")[1])
    filename = config.config["storage"]["imageUploadPath"] + "/" + file
    log.info("Saving image to {}".format(filename))
    with open(filename, "wb") as out_file:
        out_file.write(data)

    res = response("success", "upload image success", False)
    res["imagePath"] = config.config["storage"]["imageBasePath"] + "/" + file
    return res, 200
Пример #3
0
    def get(self, user):
        """Get all bee logs for a user. Supplying the user ID is not required, it's extracted from the authorization token."""
        log.debug("Getting all bee records for user {}".format(user))
        engine = database.get_engine()

        bee = database.beerecord
        query = sql.select([
            bee.c.beerecord_id, bee.c.user_id, bee.c.bee_behavior,
            bee.c.bee_dict_id, bee.c.bee_name, bee.c.coloration_head,
            bee.c.coloration_abdomen, bee.c.coloration_thorax, bee.c.gender,
            bee.c.flower_name, bee.c.city_name, bee.c.flower_shape,
            bee.c.flower_color, bee.c.loc_info,
            bee.c.time.label("date"), bee.c.record_pic_path,
            bee.c.record_video_path
        ]).where(bee.c.user_id == user)

        results = engine.execute(query)
        data = [dict(r) for r in results]
        if len(data) == 0:
            log.warning(
                "Failed to retrieve bee records for user {}".format(user))
            return response("false", "Bee Records not found!", True,
                            data=[]), 200

        # Correct date format
        for datum in data:
            datum["date"] = datum["date"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        return response("success",
                        "Retrieve all Bee Records",
                        False,
                        data=data), 200
Пример #4
0
	def get(self):
		"""Legacy flowerlist endpoint"""
		log.debug("Getting list of all flowers in the flowerdict")
		engine = database.get_engine()
		results = engine.execute(sql.select([database.flowerdict]))
		data = [dict(r) for r in results]

		if len(data) == 0:
			log.error("Failed to retrieve list of flowers from the flowerdict")
			return response("false", "Flower List not found!", True), 404
		return response("success", "Retrieve the Flower List  success!", False, data=data), 200  # TODO fix typos
Пример #5
0
	def delete(self, id: int, user=None):
		"""Delete flower by ID"""
		log.info("Deleting flower {}".format(id))
		engine = database.get_engine()
		query = sql.delete(database.flower).where(database.flower.c.flower_id == id)
		results = engine.execute(query)

		if results.rowcount == 0:
			log.warning("Attempted to delete unknown flower #{}".format(id))
			return response("false", "flower id not found!", True), 404
		return response("success", "Delete flower success!", False), 200
Пример #6
0
	def post(self, user=None):
		"""Create a new flower."""
		args = new_flower_parser.parse_args()
		log.info("Adding flower {}".format(args))

		engine = database.get_engine()
		query = sql.insert(database.flower).values(**args)
		id = engine.execute(query).inserted_primary_key[0]  # Not all SQL engines support RETURNING

		if id is None:
			log.error("Failed to log new flower {}".format(args))
			return response("false", "Log a new flower failed", True), 405
		return response("success", "Log a new flower success!", False, data=[{"flower_id": id}]), 200
Пример #7
0
	def put(self, id: int, user=None):
		"""Update a flower by ID"""
		args = update_flower_parser.parse_args()
		args = {k: v for k, v in args.items() if v is not None}

		log.info("Updating flower {}".format(id))
		engine = database.get_engine()
		query = sql.update(database.flower).values(**args).where(database.flower.c.flower_id == id)
		results = engine.execute(query)

		if results.rowcount == 0:
			log.warning("Failed to update unknown flower #{}".format(id))
			return response("false", "Flower not found!", True), 404
		return response("success", "Update the Folwer information success!", False, data=[{"flower_id": id}]), 200  # TODO Fix typo
Пример #8
0
	def get(self):
		"""Get all flower shapes.

		This endpoint's name is something of a misnomer, it's more appropriately flower **features**."""
		log.debug("Retrieving list of all flower shapes")
		engine = database.get_engine()
		features = database.features
		results = engine.execute(sql.select([features]).where(features.c.feature_id.like("fc%")))

		data = [dict(r) for r in results]
		if len(data) == 0:
			log.warning("Failed to retrieve list of flower shapes")
			return response("false", "Flower shapes not found!", True), 404
		return response("success", "Retrieve the flower shapes success!", False, data=data), 200
Пример #9
0
    def get(self):
        """Enroll; uses a firebase token via basic auth to get a generate an access token.

		Enrolling will result in removal of all other enrollments from the authentication database, i.e. all other
		sessions for this user are automatically logged out."""
        if "Authorization" not in request.headers or request.headers[
                "Authorization"].find("Basic") == -1:
            log.warning(
                "User tried to enroll but didn't present a Firebase auth token"
            )
            return response("false", "Firebase authorization token required",
                            True), 403
        try:
            token = base64.standard_b64decode(
                request.headers["Authorization"].split(" ")[1])
            uid = auth.verify_id_token(token)["uid"]
        except Exception as e:
            log.warning("Firebase token failed validation: {}".format(e))
            return response("false", "Firebase token failed validation",
                            True), 403

        # Generate token (1024 random bytes), expiration,
        accessToken = secrets.token_hex(1024)
        refreshToken = secrets.token_hex(1024)
        expiration = datetime.now() + timedelta(
            seconds=config.config["auth"]["token-lifetime"])

        # Delete all other entries for this user and insert a new auth table entry. This effectively logs the user out
        # of all other devices, and helps keep the auth table clean.
        engine = database.get_engine()
        engine.execute(
            sql.delete(database.auth).where(database.auth.c.user_id == uid))
        engine.execute(
            sql.insert(database.auth).values(
                user_id=uid,
                access_token=accessToken,
                refresh_token=refreshToken,
                token_expiry=expiration.timestamp() * 1000))
        log.debug("Enrolled user {}".format(uid))

        return {
            "accessToken": accessToken,
            "refreshToken": refreshToken,
            "expiresIn": config.config["auth"]["token-lifetime"] * 1000,
            "expiresAt": int(expiration.timestamp() * 1000),
            "type": "Bearer"
        }, 200
Пример #10
0
    def delete(self, id: int, user):
        """Delete a bee record by ID"""
        log.debug("Deleting bee record #{}".format(id))
        engine = database.get_engine()
        query = sql.delete(
            database.beerecord).where(database.beerecord.c.beerecord_id == id)
        results = engine.execute(query)

        if results.rowcount == 0:
            log.warning("Failed to delete bee record #{}".format(id))
            return response("false", "Bee record id not found!", True), 200
        return response("success",
                        "Delete record success!",
                        False,
                        data=[{
                            "beerecord_id": id
                        }]), 200
Пример #11
0
def update_news_file(news, filename):
	try:
		with open("{}/{}".format(config.config["storage"]["news-path"], filename), "w") as file:
			json.dump(news, file)
	except IOError as e:
		log.error("Failed to save bio/cs news update to {}: {}".format(config.config["storage"]["news-path"], e))
		return "Failed to save news update", 500
	return response("success", "Updated news", False)
Пример #12
0
    def get(id: int = -1):
        """Get an entry from the beedex by ID. If no ID is provided, all beedex entries are returned."""
        log.debug("Getting beedex entry #{}".format(id if id != -1 else "*"))
        engine = database.get_engine()
        query = sql.select([database.beedict])
        if id != -1:
            query = query.where(database.beedict.c.bee_id == id)
        results = engine.execute(query)

        data = [dict(r) for r in results]
        if len(data) == 0:
            log.warning("Failed to retrieve entry #{} from beedex".format(id))
            return response("false", "Bee Dexes not found!", True), 200
        log.debug("Returning {} beedex entries".format(len(data)))
        return response("success",
                        "Retrieve the Bee information success!",
                        False,
                        data=data), 200
Пример #13
0
	def get(self):
		"""Get Bio/CS news. News is contained in the `data` field of the response."""
		data = {}
		try:
			with open("{}/biocsnews.json".format(config.config["storage"]["news-path"]), "r") as file:
				data = json.load(file)
		except IOError as e:
			log.warning("Failed to load news file {}/biocsnews.json returning default response. Error: {}".format(config.config["storage"]["news-path"], e))
		return response("success", "Retrieve the BIO-CS News information success!", True, data=data), 200
Пример #14
0
	def get(self, id: int = -1):
		"""Get flower by ID"""
		log.debug("Getting flowerdex ID {}".format(id if id != -1 else "*"))
		engine = database.get_engine()
		flower = database.flowerdict
		query = sql.select([flower.c.flower_id,
		                    flower.c.latin_name.label("flower_latin_name"),
		                    flower.c.main_common_name.label("flower_common_name")])
		if id != -1:
			query = query.where(database.flowerdict.c.flower_id == id)
		results = engine.execute(query)

		data = [dict(r) for r in results]
		if len(data) == 0:
			log.warning("Failed to retrieve flower #{}".format(id))
			return response("false", "Flower not found!", True), 404

		log.debug("Returning {} flowerdex entries".format(len(data)))
		return response("success", "Retrieve the Flower information success!", False, data=data), 200
Пример #15
0
    def post(self, user):
        """Record a new bee log. Requires user login."""

        # Optional params that aren't specified in the v5 API but are in the bee web app API interface
        args = bee_record_parser.parse_args()

        # Terrible, terrible hack because the web app is broken and submits STRING DATES instead of beedictids
        # The old API didn't parse it correctly (i.e. throw a hissy fit because of an incorrect data type) and just
        # inserted the first number it could find in the date -- the year. This "workaround" makes this API mimic the
        # behavior of the old one. GOD @$%!ING DAMNIT I HATE MY LIFE
        # TODO UNFUCK BEEDICTID POSTING WHEN THE WEBAPP IS FIXED
        args["bee_dict_id"] = datetime.now().year

        log.info("User {} logging new bee record {}".format(user, args))

        # Convert time, bee_behavior
        try:
            args["time"] = datetime.strptime(args["time"],
                                             "%Y-%m-%dT%H:%M:%S.%fZ")
        except ValueError:
            return response("false", "Invalid date", True), 400

        if args["bee_behavior"] > 2 or args["bee_behavior"] < 0:
            return response("false", "Invalid beebehavior", True), 400
        args["bee_behavior"] = ["unknown", "nectar",
                                "pollen"][args["bee_behavior"]]

        engine = database.get_engine()
        query = sql.insert(database.beerecord).values(**args, user_id=user)
        results = engine.execute(
            query)  # Not all SQL engines support RETURNING

        # TODO re-implement proper ID returning
        # if id is None:
        # 	log.error("User {} failed to log new bee record {}".format(user, args))
        # 	return response("false", "Log a new bee failed", True), 405
        return response("success",
                        "Log a new bee success!",
                        False,
                        data=[{
                            "beerecord_id": 5
                        }]), 200
Пример #16
0
    def get(self):
        """Get all bee records (reduced).

		**Warning!** This returns an *extremely* large dataset, literally the entire contents of the database!
		Historically this endpoint has responded with several *mega*bytes of data, so bee **careful!**
		"""
        log.debug("Getting all bee records")
        engine = database.get_engine()
        bee = database.beerecord
        flower = database.flowerdict
        # TODO Remove duplicate gender entry (requires modifications to sites that rely on this endpoint)
        query = sql.select([
            bee.c.bee_name,
            bee.c.time.label("date"), bee.c.loc_info, bee.c.elevation,
            flower.c.shape.label("flower_shape"),
            flower.c.main_common_name.label("flower_name"),
            flower.c.main_color.label("flower_color"), bee.c.bee_behavior,
            bee.c.gender.label("spgender"),
            case([(func.lower(bee.c.gender) == "queen"
                   or func.lower(bee.c.gender) == "worker"
                   or func.lower(bee.c.gender) == "female", "Female"),
                  (func.lower(bee.c.gender) == "male", "Male"),
                  (func.lower(bee.c.gender) == "male/female", "unknown"),
                  (func.lower(bee.c.gender) == "unknown", "unknown")],
                 else_=bee.c.gender).label("gender")
        ]).select_from(
            bee.join(flower, flower.c.latin_name == bee.c.flower_name))
        results = engine.execute(query)

        data = [dict(r) for r in results]
        if len(data) == 0:
            log.error("Failed to retrieve bee records for beevisrecords")
            return response("false", "Bee records not found!", True), 200

        # Correct date format
        for datum in data:
            datum["date"] = datum["date"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")

        return response("success",
                        "Retrieve the Bee records success!",
                        False,
                        data=data), 200
Пример #17
0
    def get(self):
        """Obtain a new access token using a refresh token."""
        if "Authorization" not in request.headers or request.headers[
                "Authorization"].find("Bearer") == -1:
            return response("false", "Authorization required", True), 403
        try:
            decoded = base64.standard_b64decode(
                request.headers["Authorization"].split(" ")[1])
            refresh_token, firebase_token = decoded.decode("utf-8").split(":")
            uid = auth.verify_id_token(firebase_token)["uid"]
        except Exception as e:
            log.warning("Firebase token failed validation".format(e))
            return response("false", "Firebase token failed validation",
                            True), 403

        # Verify we have an entry in the auth table corresponding to this uid+token combo
        engine = database.get_engine()
        results = engine.execute(
            sql.select([database.auth]).where(
                database.auth.c.user_id == uid
                and database.auth.c.refresh_token == refresh_token))
        if len([dict(r) for r in results]) == 0:
            log.warning(
                "Failed to validate UID + refresh token for UID {}".format(
                    uid))
            return response("false", "Failed to validate UID+refresh token",
                            True), 403

        # Generate a new access token and expiration time, update them in the database
        access_token = secrets.token_hex(1024)
        expiration = datetime.now() + timedelta(
            seconds=config.config["auth"]["token-lifetime"])

        engine.execute(sql.update(database.auth).values(token_expiry=expiration.timestamp() * 1000,
                                                        access_token=access_token) \
                       .where(and_(database.auth.c.refresh_token == refresh_token.decode("utf-8"), database.auth.c.user_id == uid)))

        return {
            "accessToken": access_token,
            "expiresIn": config.config["auth"]["token-lifetime"] * 1000,
            "expiresAt": int(expiration.timestamp() * 1000)
        }, 200
Пример #18
0
    def get(self):
        """Get bee records for which there is no elevation info"""
        engine = database.get_engine()
        results = engine.execute(
            sql.select([database.beerecord
                        ]).where(database.beerecord.c.elevation.is_(None)))
        data = [dict(r) for r in results]
        if len(data) == 0:
            log.info(
                "Failed to find bee records without elevations. This may be intended, depending on data quality."
            )
            return response("false", "No Elevation Records not found!",
                            True), 200

        # Correct date format
        for datum in data:
            datum["time"] = datum["time"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        return response("success",
                        "Retrieve the No Elevation Records success!",
                        False,
                        data=data), 200
Пример #19
0
    def put(self, id: int, user=None):
        """Update a bee record by ID"""
        args = bee_record_update_parser.parse_args()
        args = {k: v
                for k, v in args.items()
                if v is not None}  # Eliminate "None" args
        log.debug("Updating bee record {}".format(id))

        engine = database.get_engine()
        query = sql.update(database.beerecord).values(**args).where(
            database.beerecord.c.beerecord_id == id)
        results = engine.execute(query)

        if results.rowcount == 0:
            log.warning("Failed to update bee record #{}".format(id))
            return response("false", "Bee Dexes not found!", True), 200
        return response("success",
                        "Retrieve the Bee information success!",
                        False,
                        data=[{
                            "beerecord_id": id
                        }]), 200
Пример #20
0
    def get(self, page: int, user=None):
        """Get bee records by page (segments of 50). Requires administrator access."""
        log.debug("Getting page {} of bee records".format(page))
        engine = database.get_engine()
        bee = database.beerecord
        flower = database.flowerdict

        query = sql.select([
            bee.c.beerecord_id, bee.c.bee_dict_id, bee.c.bee_name,
            bee.c.coloration_abdomen, bee.c.coloration_thorax,
            bee.c.coloration_head,
            flower.c.shape.label("flower_shape"),
            flower.c.colors.label("flower_color"), bee.c.time, bee.c.loc_info,
            bee.c.user_id, bee.c.record_pic_path, bee.c.record_video_path,
            bee.c.flower_name, bee.c.city_name, bee.c.gender,
            bee.c.bee_behavior, flower.c.common_name, bee.c.app_version,
            bee.c.elevation
        ]).select_from(
            bee.join(flower,
                     bee.c.flower_name == flower.c.latin_name,
                     isouter=True)).where(
                         bee.c.user_id != "*****@*****.**"
                         and bee.c.user_id != "*****@*****.**").order_by(
                             bee.c.beerecord_id.desc()).limit(50).offset(
                                 50 * (page - 1))

        results = engine.execute(query)
        data = [dict(r) for r in results]
        if len(data) == 0:
            log.error("Failed to retrieve bee records for beerecords")
            return response("false", "Bee records not found!", True), 200

        # Correct date format
        for datum in data:
            datum["time"] = datum["time"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        return response("success",
                        "Retrieve the Bee records success!",
                        False,
                        data=data), 200
Пример #21
0
	def get(self):
		"""Get a list of recorded flowers that aren't in the flowerdex.

		List is sorted in alphabetical order by flower name."""
		log.debug("Retrieving list of unmatched flowers")
		engine = database.get_engine()
		bee = database.beerecord
		flower = database.flowerdict
		query = sql.select([
			bee.c.flower_name,
			func.count().label("count")
		]).select_from(
			bee.join(flower, bee.c.flower_name == flower.c.latin_name, isouter=True)
		).where(
			and_(flower.c.latin_name.is_(None), bee.c.flower_name.isnot(None))
		).group_by(bee.c.flower_name).order_by(bee.c.flower_name)

		results = engine.execute(query)
		data = [dict(r) for r in results]
		if len(data) == 0:
			log.warning("Failed to retrieve list of unmatched flowers")
			return response("false", "Bee records not found!", True), 404  # TODO Change messages
		return response("success", "Retrieve the Bee records success!", False, data=data), 200
Пример #22
0
    def get(self, id: int, user=None):
        """Get a bee record by ID"""
        # Copied over from node server:
        # TODO Ask if we should scope it down to the specific user ID
        # TODO Any authenticated user can access any record ID
        log.debug("Getting bee record with ID")
        engine = database.get_engine()
        bee = database.beerecord
        if id == -1:
            query = sql.select([
                bee.c.bee_dict_id, bee.c.bee_name, bee.c.loc_info, bee.c.time
            ])
        else:
            query = sql.select([
                bee.c.beerecord_id, bee.c.user_id, bee.c.bee_dict_id,
                bee.c.bee_name, bee.c.coloration_head,
                bee.c.coloration_abdomen, bee.c.coloration_thorax,
                bee.c.gender, bee.c.flower_name, bee.c.city_name,
                bee.c.flower_shape, bee.c.flower_color, bee.c.loc_info,
                bee.c.time.label("date"), bee.c.bee_behavior,
                bee.c.record_pic_path, bee.c.record_video_path, bee.c.elevation
            ]).where(bee.c.beerecord_id == id)

        results = engine.execute(query)
        data = [dict(r) for r in results]
        if len(data) == 0:
            log.warning("Failed to retrieve bee records for beerecord")
            return response("false", "Bee Records not found!", True), 200

        # Correct date format
        for datum in data:
            datum["date"] = datum["date"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
        return response("success",
                        "Retrieve the Bee records success!",
                        False,
                        data=data), 200
Пример #23
0
    def admin_wrapper(*args, **kwargs):
        # Allow unit tests to skip admin guards
        if "testing" in config.config and config.config["testing"]:
            return func(*args, **kwargs)

        # Check administrator table to see if there's an entry for this user. If there isn't, return an error.
        engine = database.get_engine()
        results = engine.execute(
            sql.select([database.admin
                        ]).where(database.admin.c.user_id == kwargs["user"]))
        if len(next(results, {})) == 0:
            return response("false", "Administrator access required",
                            True), 403

        log.debug("Authenticated admin \"{}\"".format(kwargs["user"]))
        return func(*args, **kwargs)