Skip to content

Commit

Permalink
bug 1096528: backend support for new balrog ui. r=mgerva
Browse files Browse the repository at this point in the history
  • Loading branch information
bhearsum committed Nov 13, 2014
1 parent 72d815b commit 4338af9
Show file tree
Hide file tree
Showing 16 changed files with 718 additions and 295 deletions.
37 changes: 30 additions & 7 deletions auslib/admin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
from auslib.admin.views.permissions import UsersView, PermissionsView, \
SpecificPermissionView, PermissionsPageView, UserPermissionsPageView
from auslib.admin.views.releases import SingleLocaleView, SingleBlobView, \
SingleReleaseView, ReleasesPageView, ReleaseHistoryView
SingleReleaseView, ReleasesPageView, ReleaseHistoryView, \
ReleasesAPIView
from auslib.admin.views.rules import RulesPageView, RulesAPIView, \
SingleRuleView, RuleHistoryView
SingleRuleView, RuleHistoryView, RuleHistoryAPIView
from auslib.admin.views.history import DiffView, FieldView
from auslib.admin.views.index import IndexPageView, RecentChangesTableView

Expand All @@ -39,24 +40,46 @@ def add_security_headers(response):

Compress(app)

# Endpoints required for the Balrog 2.0 UI.
app.add_url_rule("/api/csrf_token", view_func=CSRFView.as_view("api_csrf"))
app.add_url_rule("/api/users", view_func=UsersView.as_view("api_users"))
app.add_url_rule("/api/users/<username>/permissions", view_func=PermissionsView.as_view("api_user_permissions"))
app.add_url_rule("/api/users/<username>/permissions/<path:permission>", view_func=SpecificPermissionView.as_view("api_specific_permission"))
# Some permissions may start with a slash, and the <path> converter won"t match them, so we need an extra rule to cope.
app.add_url_rule("/api/users/<username>/permissions//<path:permission>", view_func=SpecificPermissionView.as_view("api_specific_permission2"))
app.add_url_rule("/api/rules", view_func=RulesAPIView.as_view("api_rules"))
app.add_url_rule("/api/rules/<rule_id>", view_func=SingleRuleView.as_view("api_rule"))
app.add_url_rule("/api/rules/<rule_id>/revisions", view_func=RuleHistoryAPIView.as_view("api_rules_revisions"))
app.add_url_rule("/api/releases", view_func=ReleasesAPIView.as_view("api_releases"))
app.add_url_rule("/api/releases/<release>", view_func=SingleReleaseView.as_view("api_releases_revision"))
app.add_url_rule("/api/releases/<release>/builds/<platform>/<locale>", view_func=SingleLocaleView.as_view("api_single_locale"))
app.add_url_rule("/api/releases/<release>/revisions", view_func=ReleaseHistoryView.as_view("api_release_revisions"))
app.add_url_rule("/api/history/diff/<type_>/<change_id>/<field>", view_func=DiffView.as_view("api_diff"))
app.add_url_rule("/api/history/view/<type_>/<change_id>/<field>", view_func=FieldView.as_view("api_field"))


# Deprecated endpoints. These can be removed when the old UI is shut off _and_ submitter tools use the new "/api" endpoints.
app.add_url_rule('/csrf_token', view_func=CSRFView.as_view('csrf'))
app.add_url_rule('/users', view_func=UsersView.as_view('users'))
app.add_url_rule('/users/<username>/permissions', view_func=PermissionsView.as_view('permissions'))
app.add_url_rule('/users/<username>/permissions/<path:permission>', view_func=SpecificPermissionView.as_view('specific_permission'))
# Some permissions may start with a slash, and the <path> converter won't match them, so we need an extra rule to cope.
app.add_url_rule('/users/<username>/permissions//<path:permission>', view_func=SpecificPermissionView.as_view('specific_permission2'))
app.add_url_rule('/permissions.html', view_func=PermissionsPageView.as_view('permissions.html'))
app.add_url_rule('/user_permissions.html', view_func=UserPermissionsPageView.as_view('user_permissions.html'))
app.add_url_rule('/releases/<release>/builds/<platform>/<locale>', view_func=SingleLocaleView.as_view('single_locale'))
app.add_url_rule('/releases/<release>/data', view_func=SingleBlobView.as_view('release_data'))
app.add_url_rule('/releases/<release>/revisions/', view_func=ReleaseHistoryView.as_view('release_revisions'))
app.add_url_rule('/releases/<release>', view_func=SingleReleaseView.as_view('release'))
app.add_url_rule('/releases.html', view_func=ReleasesPageView.as_view('releases.html'))
app.add_url_rule('/rules.html', view_func=RulesPageView.as_view('rules.html'))
app.add_url_rule('/rules', view_func=RulesAPIView.as_view('rules'))
app.add_url_rule('/rules/<rule_id>/revisions/', view_func=RuleHistoryView.as_view('revisions.html'))
app.add_url_rule('/rules/<rule_id>', view_func=SingleRuleView.as_view('setrule'))
app.add_url_rule('/history/diff/<type_>/<change_id>/<field>', view_func=DiffView.as_view('diff'))
app.add_url_rule('/history/view/<type_>/<change_id>/<field>', view_func=FieldView.as_view('field'))


# TODO: Kill these endpoints + their classes when the old admin ui is shut off
app.add_url_rule('/permissions.html', view_func=PermissionsPageView.as_view('permissions.html'))
app.add_url_rule('/user_permissions.html', view_func=UserPermissionsPageView.as_view('user_permissions.html'))
app.add_url_rule('/releases.html', view_func=ReleasesPageView.as_view('releases.html'))
app.add_url_rule('/rules.html', view_func=RulesPageView.as_view('rules.html'))
app.add_url_rule('/recent_changes_table.html', view_func=RecentChangesTableView.as_view(''))
app.add_url_rule('/rules/<rule_id>/revisions/', view_func=RuleHistoryView.as_view('revisions.html'))
app.add_url_rule('/', view_func=IndexPageView.as_view('index.html'))
7 changes: 5 additions & 2 deletions auslib/admin/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ def decorated(*args, **kwargs):
method = request.method
extra = dict()
for opt in options:
if opt not in request.form:
if opt in request.form:
extra[opt] = request.form[opt]
elif request.get_json() and opt in request.json:
extra[opt] = request.json[opt]
else:
msg = "Couldn't find required option %s in form" % opt
cef_event("Bad input", CEF_WARN, msg=msg)
return Response(status=400, response=msg)
extra[opt] = request.form[opt]
if not dbo.permissions.hasUrlPermission(username, url, method, urlOptions=extra):
msg = "%s is not allowed to access %s by %s" % (username, url, method)
cef_event('Unauthorized access attempt', CEF_ALERT, msg=msg)
Expand Down
2 changes: 1 addition & 1 deletion auslib/admin/views/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ def get_csrf_headers():
return {'X-CSRF-Token': form.csrf_token._value()}

class CSRFView(AdminView):
"""/csrf"""
"""/api/csrf_token"""
def get(self):
return Response(headers=get_csrf_headers())
1 change: 1 addition & 0 deletions auslib/admin/views/index.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# TODO: Kill me when old admin ui is shut off
import collections

from flask import render_template, request
Expand Down
20 changes: 13 additions & 7 deletions auslib/admin/views/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,26 @@ def permission2selector(permission):
return permission.replace('/', '').replace(':', '')

class UsersView(AdminView):
"""/users"""
"""/api/users"""
def get(self):
users = dbo.permissions.getAllUsers()
self.log.debug("Found users: %s", users)
# TODO: Only return json after old ui is dead
fmt = request.args.get('format', 'html')
if fmt == 'json':
if fmt == 'json' or 'application/json' in request.headers.get('Accept'):
# We don't return a plain jsonify'ed list here because of:
# http://flask.pocoo.org/docs/security/#json-security
return jsonify(dict(users=users))
else:
return render_template('fragments/users.html', users=users)
return render_template('fragments/users.html', users=users.keys())

class PermissionsView(AdminView):
"""/users/[user]/permissions"""
"""/api/users/:username/permissions"""
def get(self, username):
permissions = dbo.permissions.getUserPermissions(username)
# TODO: Only return json after old ui is dead
fmt = request.args.get('format', 'html')
if fmt == 'json':
if fmt == 'json' or 'application/json' in request.headers.get('Accept'):
return jsonify(permissions)
else:
forms = []
Expand All @@ -48,13 +50,14 @@ def get(self, username):
return render_template('fragments/user_permissions.html', username=username, permissions=forms)

class SpecificPermissionView(AdminView):
"""/users/[user]/permissions/[permission]"""
"""/api/users/:username/permissions/:permission"""
@setpermission
def get(self, username, permission):
try:
perm = dbo.permissions.getUserPermissions(username)[permission]
except KeyError:
return Response(status=404)
# TODO: Only return json after old ui is dead
fmt = request.args.get('format', 'html')
if fmt == 'json':
return jsonify(perm)
Expand Down Expand Up @@ -112,9 +115,12 @@ def _delete(self, username, permission, changed_by, transaction):
dbo.permissions.revokePermission(changed_by, username, permission, form.data_version.data, transaction=transaction)
return Response(status=200)
except ValueError, e:
cef_event("Bad input", CEF_WARN, e.args)
cef_event("Bad input", CEF_WARN, errors=e.args)
return Response(status=400, response=e.args)



# TODO: Kill me when old admin ui is shut off
class PermissionsPageView(AdminView):
"""/permissions.html"""
def get(self):
Expand Down
144 changes: 111 additions & 33 deletions auslib/admin/views/releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,9 @@ def changeRelease(release, changed_by, transaction, existsCallback, commitCallba
form = ReleaseForm()
if not form.validate():
cef_event("Bad input", CEF_WARN, errors=form.errors)
return Response(status=400, response=form.errors)
return Response(status=400, response=json.dumps(form.errors))
product = form.product.data
version = form.version.data
hashFunction = form.hashFunction.data
incomingData = form.data.data
copyTo = form.copyTo.data
alias = form.alias.data
Expand All @@ -85,6 +84,13 @@ def changeRelease(release, changed_by, transaction, existsCallback, commitCallba
schema_version = incomingData.get("schema_version")
else:
return Response(status=400, response="schema_version is required")

if getattr(form.hashFunction, "data", None):
hashFunction = form.hashFunction.data
elif incomingData.get("hashFunction"):
hashFunction = incomingData.get("hashFunction")
else:
hashFunction = None

allReleases = [release]
if copyTo:
Expand All @@ -108,7 +114,7 @@ def changeRelease(release, changed_by, transaction, existsCallback, commitCallba
msg = "Product name '%s' doesn't match the one on the release object ('%s') for release '%s'" % (product, releaseInfo['product'], rel)
cef_event("Bad input", CEF_WARN, errors=msg, release=rel)
return Response(status=400, response=msg)
if 'hashFunction' in releaseInfo['data'] and hashFunction != releaseInfo['data']['hashFunction']:
if 'hashFunction' in releaseInfo['data'] and hashFunction and hashFunction != releaseInfo['data']['hashFunction']:
msg = "hashFunction '%s' doesn't match the one on the release object ('%s') for release '%s'" % (hashFunction, releaseInfo['data']['hashFunction'], rel)
cef_event("Bad input", CEF_WARN, errors=msg, release=rel)
return Response(status=400, response=msg)
Expand Down Expand Up @@ -162,12 +168,12 @@ def changeRelease(release, changed_by, transaction, existsCallback, commitCallba


class SingleLocaleView(AdminView):
"""/releases/[release]/builds/[platform]/[locale]"""
"""/api/releases/[release]/builds/[platform]/[locale]"""
def get(self, release, platform, locale):
try:
locale = dbo.releases.getLocale(release, platform, locale)
except KeyError, e:
return Response(status=404, response=e.args)
return Response(status=404, response=json.dumps(e.args), mimetype="application/json")
data_version = dbo.releases.getReleases(name=release)[0]['data_version']
headers = {'X-Data-Version': data_version}
headers.update(get_csrf_headers())
Expand Down Expand Up @@ -196,38 +202,16 @@ def commit(rel, product, version, localeData, releaseData, old_data_version, ext

return changeRelease(release, changed_by, transaction, exists, commit, self.log)

class ReleasesPageView(AdminView):
""" /releases.html """
def get(self):
releases = dbo.releases.getReleaseInfo()
addForm = NewReleaseForm(prefix="new_release")
forms = {}
for r in releases:
release = r["name"]
forms[release] = DbEditableForm(
prefix=release,
data_version=r["data_version"],
)
return render_template('releases.html', releases=releases, addForm=addForm, forms=forms)

class SingleBlobView(AdminView):
""" /releases/[release]/data"""
def get(self, release):
try:
release_blob = dbo.releases.getReleaseBlob(name=release)
return jsonify(release_blob)
except KeyError:
return Response(status=404)

class SingleReleaseView(AdminView):
""" /releases/[release]"""
""" /api/releases/:release"""
def get(self, release):
release = dbo.releases.getReleases(name=release, limit=1)
if not release:
return Response(status=404)
return Response(status=404, mimetype="application/json")
headers = {'X-Data-Version': release[0]['data_version']}
headers.update(get_csrf_headers())
if 'application/json' in request.headers.get('Accept-Encoding', ''):
# TODO: Only return json after old ui is dead
if 'application/json' in request.headers.get('Accept', ''):
return Response(response=json.dumps(release[0]['data']), mimetype='application/json', headers=headers)
else:
form = DbEditableForm(prefix=release[0]['name'], data_version=release[0]['data_version'])
Expand Down Expand Up @@ -294,9 +278,9 @@ def _delete(self, release, changed_by, transaction):

return Response(status=200)

class ReleaseHistoryView(HistoryAdminView):
""" /releases/<release>/revisions/ """

class ReleaseHistoryView(HistoryAdminView):
"""/api/releases/:release/revisions"""
def get(self, release):
releases = dbo.releases.getReleases(name=release, limit=1)
if not releases:
Expand Down Expand Up @@ -337,6 +321,13 @@ def get(self, release):

self.annotateRevisionDifferences(revisions)

# TODO: Only return json after old ui is dead
if 'application/json' in request.headers.get('Accept', ''):
return jsonify({
'revisions': revisions,
'count': total_count,
})

return render_template(
'revisions.html',
revisions=revisions,
Expand Down Expand Up @@ -380,3 +371,90 @@ def _post(self, release, transaction, changed_by):
return Response(status=400, response=e.args)

return Response("Excellent!")


class ReleasesAPIView(AdminView):
"""/api/releases"""
def get(self, **kwargs):
if request.args.get('names_only'):
releases = dbo.releases.getReleaseInfo(nameOnly=True)
names = []
for release in releases:
names.append(release['name'])
data = {'names': names}
else:
releases = dbo.releases.getReleaseInfo()
_releases = []
_mapping = {
# return : db name
'name': 'name',
'product': 'product',
'version': 'version',
'data_version': 'data_version',
}
for release in releases:
_releases.append(dict(
(key, release[db_key])
for key, db_key in _mapping.items()
))
data = {
'releases': _releases,
}

return Response(response=json.dumps(data), mimetype="application/json")

@requirelogin
@requirepermission('/releases')
def _post(self, changed_by, transaction):
form = NewReleaseForm()
if not form.validate():
cef_event("Bad input", CEF_WARN, errors=form.errors)
return Response(status=400, response=json.dumps(form.errors))

try:
name = dbo.releases.addRelease(
name=form.name.data, product=form.product.data,
version=form.version.data, blob=form.blob.data,
changed_by=changed_by, transaction=transaction
)
except ValueError, e:
msg = "Couldn't update release: %s" % e
cef_event("Bad input", CEF_WARN, errors=msg)
return Response(status=400, response=msg)

release = dbo.releases.getReleases(
name=name, transaction=transaction, limit=1
)[0]
new_data_version = release['data_version']
response = make_response(
json.dumps(dict(new_data_version=new_data_version)),
201
)
response.headers['Content-Type'] = 'application/json'
return response


# TODO: Kill me when old admin ui is shut off
class ReleasesPageView(AdminView):
""" /releases.html """
def get(self):
releases = dbo.releases.getReleaseInfo()
addForm = NewReleaseForm(prefix="new_release")
forms = {}
for r in releases:
release = r["name"]
forms[release] = DbEditableForm(
prefix=release,
data_version=r["data_version"],
)
return render_template('releases.html', releases=releases, addForm=addForm, forms=forms)

class SingleBlobView(AdminView):
""" /releases/[release]/data"""
def get(self, release):
try:
release_blob = dbo.releases.getReleaseBlob(name=release)
return jsonify(release_blob)
except KeyError:
return Response(status=404)

Loading

0 comments on commit 4338af9

Please sign in to comment.