Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev/#107 Authentication tokens for APIs #153

Merged
merged 39 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5d3384a
testing flask db commands
tuz666 Jun 8, 2022
9559477
revert
tuz666 Jun 8, 2022
ff274af
first steps for testing flask-praetorian
tuz666 Jul 27, 2022
cfb206f
token generation is working now
tuz666 Aug 11, 2022
0d15cf3
test with flask-securitys auth token handling
tuz666 Aug 15, 2022
28182ee
hotfix
tuz666 Aug 15, 2022
8d5380e
working endpoint for the API token
tuz666 Aug 15, 2022
b358a6e
testing how it works with the CI
tuz666 Aug 15, 2022
9c09241
fixx
tuz666 Aug 15, 2022
ce55652
fix postman test
tuz666 Aug 15, 2022
c7de9ea
get_api_token working fine, show_all still returns 401
tuz666 Aug 15, 2022
1d88c33
added extra log into the postman tests
tuz666 Aug 15, 2022
8cf1583
set api_token before newman tests
tuz666 Aug 16, 2022
f39734d
we don't have to install curl
tuz666 Aug 16, 2022
3c14361
initialize api_token in separate newman run
tuz666 Aug 16, 2022
fce9b31
working hashed password
tuz666 Aug 16, 2022
a48c366
fixed arcsi view to handle tokens
tuz666 Aug 16, 2022
a2b6de9
fixed views
tuz666 Aug 16, 2022
881b47d
refining draft commit
tuz666 Aug 16, 2022
e1c33c0
fixxx
tuz666 Aug 16, 2022
097d223
added headers to some missing places
tuz666 Aug 16, 2022
3df654f
roles_required not working yet
tuz666 Aug 16, 2022
edaae1f
testing separate arcsi and api endpoint for adding shows
tuz666 Aug 24, 2022
8a26aa0
example for protected API used by a view function
tuz666 Aug 24, 2022
e184352
fixx
tuz666 Aug 24, 2022
36013d2
browser and API endpoints for show POST requests
tuz666 Aug 27, 2022
8c90e9b
add and edit helper functions for items
tuz666 Aug 27, 2022
16e25e2
separate browser and API endpoints for items
tuz666 Aug 27, 2022
2fcc1ce
fixed item edit APIs
tuz666 Aug 27, 2022
6c6b00c
fix postman test, edit item's id was not passed
tuz666 Aug 27, 2022
5da1406
removed some early dev usecases
tuz666 Aug 27, 2022
3e1df13
added token protection to the GET requests
tuz666 Aug 27, 2022
f904e55
added token headers for Postman's GET requests
tuz666 Aug 27, 2022
847a0bd
Merge branch 'master' into dev/#107-praetorian_authentication
tuz666 Aug 27, 2022
3165e93
added API token to user profile view
tuz666 Aug 28, 2022
56c8ea9
Merge branch 'dev/#107-praetorian_authentication' of github.com:mmmnm…
tuz666 Aug 28, 2022
da0220a
Add auth token for preflight mode in proxy
gammaw Sep 23, 2022
2c96300
reverting _add/_edit_show/item functions
tuz666 Oct 9, 2022
84e0973
updated Swagger docs
tuz666 Oct 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 325 additions & 21 deletions arcsi/api/item.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
from datetime import datetime, timedelta
import json
import os
import requests
import io

from mutagen.id3 import APIC, ID3
from mutagen.mp3 import MP3

from flask import flash, jsonify, make_response, request, send_file, url_for, redirect
from flask import current_app as app
from marshmallow import fields, post_load, Schema, ValidationError
from flask import jsonify, make_response, request, redirect
from flask_security import login_required, auth_token_required, roles_required
from marshmallow import fields, post_load, Schema
from sqlalchemy import func

from uuid import uuid4

from .utils import (
archive,
broadcast_audio,
dict_to_obj,
media_path,
normalise,
save_file,
item_duplications_number
)
from .utils import archive, broadcast_audio, normalise, save_file, item_duplications_number
from . import arcsi
from arcsi.handler.upload import DoArchive
from arcsi.model import db
Expand Down Expand Up @@ -126,7 +111,9 @@ def view_item(id):
return make_response("Item not found", 404, headers)


@arcsi.route("/item", methods=["POST"])
@arcsi.route("/item/add", methods=["POST"])
@login_required
@roles_required("admin")
def add_item():
no_error = True
if request.is_json:
Expand Down Expand Up @@ -291,6 +278,173 @@ def add_item():
return "Some error happened, check server logs for details. Note that your media may have been uploaded (to DO and/or Azurcast)."


@arcsi.route("/item/add_api", methods=["POST"])
@auth_token_required
@roles_required("admin")
def add_item_api():
no_error = True
if request.is_json:
return make_response(
jsonify("Only accepts multipart/form-data for now, sorry"), 503, headers
)
# work around ImmutableDict type
item_metadata = request.form.to_dict()
# TODO if we could send JSON payloads w/ ajax then this prevalidation isn't needed
item_metadata["shows"] = [
{"id": item_metadata["shows"], "name": item_metadata["show_name"]}
]
item_metadata.pop("show_name", None)
# validate payload
err = item_schema.validate(item_metadata)
if err:
return make_response(
jsonify("Invalid data sent to add item, see: {}".format(err)), 500, headers
)
else:
item_metadata = item_schema.load(item_metadata)
download_count = 0
length = 0
archived = False
image_file_name = None
image_file = None
image_url = ""
play_file = None
play_file_name = None
archive_lahmastore_canonical_url = ""
archive_mixcloud_canonical_url = ""
shows = (
db.session.query(Show)
.filter(Show.id.in_((show.id for show in item_metadata.shows)))
.all()
)

new_item = Item(
number=item_metadata.number,
name=item_metadata.name,
description=item_metadata.description,
language=item_metadata.language,
play_date=item_metadata.play_date,
image_url=image_url,
play_file_name=play_file_name,
length=length,
live=item_metadata.live,
broadcast=item_metadata.broadcast,
archive_lahmastore=item_metadata.archive_lahmastore,
archive_lahmastore_canonical_url=archive_lahmastore_canonical_url,
archive_mixcloud=item_metadata.archive_mixcloud,
archive_mixcloud_canonical_url=archive_mixcloud_canonical_url,
archived=archived,
download_count=download_count,
uploader=item_metadata.uploader,
shows=shows,
)

#Check for duplicate files
name_occurrence = item_duplications_number(new_item)

db.session.add(new_item)
db.session.flush()

# TODO get show cover img and set as fallback
if request.files:
# Defend against possible duplicate files
if name_occurrence:
version_prefix = uuid4()
item_name = "{}-{}".format(new_item.name,version_prefix)
else:
item_name = new_item.name

# process files first
if request.files["play_file"]:
if request.files["play_file"] != "":
play_file = request.files["play_file"]

new_item.play_file_name = save_file(
archive_base=new_item.shows[0].archive_lahmastore_base_url,
archive_idx=new_item.number,
archive_file=play_file,
archive_file_name=(new_item.shows[0].name, item_name),
)

if request.files["image_file"]:
if request.files["image_file"] != "":
image_file = request.files["image_file"]

image_file_name = save_file(
archive_base=new_item.shows[0].archive_lahmastore_base_url,
archive_idx=new_item.number,
archive_file=image_file,
archive_file_name=(new_item.shows[0].name, item_name),
)

if new_item.broadcast:
# we require both image and audio if broadcast (Azuracast) is set
if not (image_file_name and new_item.play_file_name):
no_error = False
# this branch is typically used for pre-uploading live episodes (no audio)
else:
if not image_file_name:
no_error = False

# archive files if asked
if new_item.archive_lahmastore:
if no_error and (play_file or image_file):
if image_file_name:
new_item.image_url = archive(
archive_base=new_item.shows[0].archive_lahmastore_base_url,
archive_file_name=image_file_name,
archive_idx=new_item.number,
)
if not new_item.image_url:
no_error = False
if new_item.play_file_name:
new_item.archive_lahmastore_canonical_url = archive(
archive_base=new_item.shows[0].archive_lahmastore_base_url,
archive_file_name=new_item.play_file_name,
archive_idx=new_item.number,
)

if new_item.archive_lahmastore_canonical_url:
# Only set archived if there is audio data otherwise it's live episode
new_item.archived = True
else: # Upload didn't succeed
no_error = False

# broadcast episode if asked
if new_item.broadcast and no_error:
if not (play_file and image_file):
no_error = False
else:
new_item.airing = broadcast_audio(
archive_base=new_item.shows[0].archive_lahmastore_base_url,
archive_idx=new_item.number,
broadcast_file_name=new_item.play_file_name,
broadcast_playlist=new_item.shows[0].playlist_name,
broadcast_show=new_item.shows[0].name,
broadcast_title=new_item.name,
image_file_name=image_file_name,
)
if not new_item.airing:
no_error = False

# TODO some mp3 error
# TODO Maybe I used vanilla mp3 not from azuracast
# item_audio_obj = MP3(item_path)
# return item_audio_obj.filename
# item_length = item_audio_obj.info.length

db.session.commit()
# TODO no_error is just bandaid for proper exc handling
if no_error:
return make_response(
jsonify(item_schema.dump(new_item)),
200,
headers,
)
else:
return "Some error happened, check server logs for details. Note that your media may have been uploaded (to DO and/or Azurcast)."


@arcsi.route("item/<id>/listen", methods=["GET"])
def listen_play_file(id):
do = DoArchive()
Expand All @@ -314,6 +468,7 @@ def download_play_file(id):


@arcsi.route("/item/<id>", methods=["DELETE"])
@auth_token_required
def delete_item(id):
item_query = Item.query.filter_by(id=id)
item = item_query.first_or_404()
Expand All @@ -322,7 +477,9 @@ def delete_item(id):
return make_response("Deleted item successfully", 200, headers)


@arcsi.route("/item/<id>", methods=["POST"])
@arcsi.route("/item/<id>/edit", methods=["POST"])
@login_required
@roles_required("admin")
def edit_item(id):
no_error = True
image_file = None
Expand Down Expand Up @@ -467,6 +624,153 @@ def edit_item(id):
return "Some error happened, check server logs for details. Note that your media may have been uploaded (to DO and/or Azurcast)."


@arcsi.route("/item/<id>/edit_api", methods=["POST"])
@auth_token_required
@roles_required("admin")
def edit_item_api(id):
no_error = True
image_file = None
image_file_name = None
play_file = None

item_query = Item.query.filter_by(id=id)
item = item_query.first_or_404()

# work around ImmutableDict type
item_metadata = request.form.to_dict()

# TODO if we could send JSON payloads w/ ajax then this prevalidation isn't needed
item_metadata["shows"] = [
{"id": item_metadata["shows"], "name": item_metadata["show_name"]}
]
item_metadata.pop("show_name", None)

# validate payload
# TODO handle what happens on f.e: empty payload?
# if err: -- need to check files {put IMG, put AUDIO} first
err = item_schema.validate(item_metadata)
if err:
return make_response(
jsonify("Invalid data sent to edit item, see: {}".format(err)),
500,
headers,
)
else:
# TODO edit uploaded media -- remove re-up etc.
# TODO broadcast / airing
item_metadata = item_schema.load(item_metadata)

#Check for duplicate files (before item is updated!)
name_occurrence = item_duplications_number(item_metadata)

item.number = item_metadata.number
item.name = item_metadata.name
item.description = item_metadata.description
item.language = item_metadata.language
item.play_date = item_metadata.play_date
item.live = item_metadata.live
item.broadcast = item_metadata.broadcast
item.airing = item_metadata.airing
item.uploader = item_metadata.uploader
item.archive_lahmastore = item_metadata.archive_lahmastore
item.archive_mixcloud = item_metadata.archive_mixcloud

# conflict between shows from detached object load(item_metadata) added to session vs original persistent object item from query
item.shows = (
db.session.query(Show)
.filter(Show.id.in_((show.id for show in item_metadata.shows)))
.all()
)

db.session.add(item)
db.session.flush()

if request.files:
# Defend against possible duplicate files
if name_occurrence:
version_prefix = uuid4()
item_name = "{}-{}".format(item.name,version_prefix)
else:
item_name = item.name

# process files first
if request.files["image_file"]:
if request.files["image_file"] != "":
image_file = request.files["image_file"]

image_file_name = save_file(
archive_base=item.shows[0].archive_lahmastore_base_url,
archive_idx=item.number,
archive_file=image_file,
archive_file_name=(item.shows[0].name, item_name),
)

if request.files["play_file"]:
if request.files["play_file"] != "":
play_file = request.files["play_file"]

item.play_file_name = save_file(
archive_base=item.shows[0].archive_lahmastore_base_url,
archive_idx=item.number,
archive_file=play_file,
archive_file_name=(item.shows[0].name, item_name),
)
if item.broadcast:
# we require both image and audio if broadcast (Azuracast) is set
if not (image_file_name and item.play_file_name):
no_error = False
# this branch is typically used for pre-uploading live episodes (no audio)
else:
if not image_file_name:
no_error = False

# archive files if asked
if item.archive_lahmastore:
if no_error and (play_file or image_file):
if image_file_name:
item.image_url = archive(
archive_base=item.shows[0].archive_lahmastore_base_url,
archive_file_name=image_file_name,
archive_idx=item.number,
)
if not item.image_url:
no_error = False
if item.play_file_name:
item.archive_lahmastore_canonical_url = archive(
archive_base=item.shows[0].archive_lahmastore_base_url,
archive_file_name=item.play_file_name,
archive_idx=item.number,
)
# Only set archived if there is audio data otherwise it's live episode
if item.archive_lahmastore_canonical_url:
item.archived = True
else:
no_error = False
# broadcast episode if asked
if item.broadcast and no_error:
if not (play_file and image_file):
no_error = False
else:
item.airing = broadcast_audio(
archive_base=item.shows[0].archive_lahmastore_base_url,
archive_idx=item.number,
broadcast_file_name=item.play_file_name,
broadcast_playlist=item.shows[0].playlist_name,
broadcast_show=item.shows[0].name,
broadcast_title=item.name,
image_file_name=image_file_name,
)
if not item.airing:
no_error = False

db.session.commit()
if no_error:
return make_response(
jsonify(item_partial_schema.dump(item)), 200, headers
)
return "Some error happened, check server logs for details. Note that your media may have been uploaded (to DO and/or Azurcast)."


@arcsi.route("/item/search", methods=["GET"])
def search_item():
do = DoArchive()
Expand Down
Loading