Commit e0d4ebb6 authored by Patrik Dufresne's avatar Patrik Dufresne Committed by Patrik Dufresne

Start implementation of a RESTful API for Rdiffweb.

Replace the /ajax endpoint by /api.
parent 36fb1dc6
#!/usr/bin/python
# -*- coding: utf-8 -*-
# rdiffweb, A web interface to rdiff-backup repositories
# Copyright (C) 2017 rdiffweb contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Created on Nov 16, 2017
@author: Patrik Dufresne
"""
from __future__ import unicode_literals
import cherrypy
import logging
from rdiffweb.core import Component
# Define the logger
logger = logging.getLogger(__name__)
class ApiCurrentuser(Component):
@cherrypy.expose
@cherrypy.tools.json_out()
def repos(self):
"""
Return the list of repositories.
"""
return self.app.currentuser.repos
@cherrypy.expose
@cherrypy.tools.json_out()
def index(self):
u = self.app.currentuser
return {
"is_admin": u.is_admin,
"email": u.email,
"user_root": u.user_root,
"repos": u.repos,
"username": u.username,
}
@cherrypy.config(**{'tools.authform.on': False, 'tools.i18n.on': False, 'tools.authbasic.on': True, 'tools.sessions.on': True, 'error_page.default': False})
class ApiPage(Component):
"""
This class provide a restful API to access some of the rdiffweb resources.
"""
def __init__(self, app):
Component.__init__(self, app)
self.currentuser = ApiCurrentuser(app)
@cherrypy.expose
@cherrypy.tools.json_out()
def index(self):
return "ok"
......@@ -31,12 +31,27 @@ from rdiffweb.core import RdiffError, RdiffWarning
from rdiffweb.i18n import ugettext as _
from rdiffweb.page_main import MainPage
from rdiffweb.rdw_helpers import quote_url
from cherrypy.lib import httpauth
# Define the logger
logger = logging.getLogger(__name__)
def check_username_and_password(username, password):
"""Validate user credentials."""
logger.debug("check credentials for [%s]", username)
try:
userobj = cherrypy.request.app.userdb.login(username, password) # @UndefinedVariable
except:
logger.exception("fail to validate user credential")
raise RdiffWarning(_("Fail to validate user credential."))
if not userobj:
logger.warning("invalid username [%s] or password", username)
raise RdiffWarning(_("Invalid username or password."))
return userobj
class AuthFormTool(HandlerTool):
"""
Tool used to control authentication to various ressources.
......@@ -49,19 +64,6 @@ class AuthFormTool(HandlerTool):
# Make sure to run after i18n tool (priority 60)
self._priority = 70
def check_username_and_password(self, username, password):
"""Validate user credentials."""
logger.debug("check credentials for [%s]", username)
try:
userobj = cherrypy.request.app.userdb.login(username, password) # @UndefinedVariable
except:
logger.exception("fail to validate user credential")
raise RdiffWarning(_("Fail to validate user credential."))
if not userobj:
logger.warning("invalid username [%s] or password", username)
raise RdiffWarning(_("Invalid username or password."))
return userobj
def do_check(self):
"""Assert username. Raise redirect, or return True if request handled."""
request = cherrypy.serving.request
......@@ -96,7 +98,7 @@ class AuthFormTool(HandlerTool):
"""Login. May raise redirect, or return True if request handled."""
response = cherrypy.serving.response
try:
userobj = self.check_username_and_password(login, password)
userobj = check_username_and_password(login, password)
except RdiffError as e:
body = self.login_screen(redirect, login, str(e))
response.body = body
......@@ -183,36 +185,37 @@ class AuthFormTool(HandlerTool):
cherrypy.tools.authform = AuthFormTool()
def authbasic(checkpassword):
def authbasic():
"""Filter used to restrict access to resource via HTTP basic auth."""
# Check if logged-in.
if cherrypy.session.get("user"): # @UndefinedVariable
# page passes credentials; allow to be processed
return False
# Proceed with basic authentication.
request = cherrypy.serving.request
auth_header = request.headers.get('authorization')
if auth_header is not None:
ah = request.headers.get('authorization')
if ah is not None:
try:
scheme, params = auth_header.split(' ', 1)
scheme, params = ah.split(' ', 1)
if scheme.lower() == 'basic':
# Validate user credential.
username, password = base64_decode(params).split(':', 1)
error_msg = checkpassword(username, password)
if error_msg:
logger.info('basic auth fail for %s: %s', username, error_msg)
else:
logger.info('basic auth succeeded for %s', username)
request.login = username
return # successful authentication
# split() error, base64.decodestring() error
try:
userobj = check_username_and_password(username, password)
except RdiffError as e:
logger.info('basic auth fail for %s', username, e)
raise cherrypy.HTTPError(403)
# User successfully login.
logger.debug('setting request.login to %s', userobj)
cherrypy.serving.request.login = userobj
return
except (ValueError, binascii.Error):
raise cherrypy.HTTPError(400, 'Bad Request')
# Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="rdiffweb"'
# Inform the user-agent this path is protected.
cherrypy.serving.response.headers[
'www-authenticate'] = httpauth.basicAuth('rdiffweb')
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
cherrypy.tools.authbasic = cherrypy._cptools.HandlerTool(authbasic)
......@@ -76,7 +76,7 @@ class RemoveOlderPlugin(ITemplateFilterPlugin, JobPlugin):
def activate(self):
# Add page
self.app.root.ajax.remove_older = RemoveOlderPage(self.app)
self.app.root.api.remove_older = RemoveOlderPage(self.app)
# Call original
ITemplateFilterPlugin.activate(self)
......
......@@ -2,7 +2,7 @@
{% import 'macros.html' as macros %}
{% call macros.panel(title=_("Remove older"), class='default') %}
<div class="panel-body">
<form data-async data-target="#keepdays-status" action="/ajax/remove-older/{{ repo_path }}/" method="POST" role="form">
<form data-async data-target="#keepdays-status" action="/api/remove-older/{{ repo_path }}/" method="POST" role="form">
<div class="form-group">
<label for="encoding" class="control-label">
{% trans %}Keep history for:{% endtrans %}</label>
......
......@@ -48,7 +48,7 @@ class RemoveOlderTest(WebCase):
self.getPage("/settings/" + repo + "/")
def _remove_older(self, repo, value):
self.getPage("/ajax/remove-older/" + repo + "/", method="POST",
self.getPage("/api/remove-older/" + repo + "/", method="POST",
body={'keepdays': value})
def test_page_set_keepdays(self):
......
......@@ -82,7 +82,7 @@ class SetEncodingPlugin(ITemplateFilterPlugin):
def activate(self):
# Add page
self.app.root.ajax.set_encoding = SetEncodingPage(self.app)
self.app.root.api.set_encoding = SetEncodingPage(self.app)
# Call original
ITemplateFilterPlugin.activate(self)
......
......@@ -23,7 +23,7 @@
<!-- Panel to set user info. -->
{% call macros.panel(title=_("Character encoding"), class='default') %}
<div class="panel-body">
<form action="/ajax/set-encoding/{{ repo_path }}/" method="POST" role="form" class="clearfix" data-async data-target="#new-encoding-status">
<form action="/api/set-encoding/{{ repo_path }}/" method="POST" role="form" class="clearfix" data-async data-target="#new-encoding-status">
<div class="form-group">
<label for="encoding" class="control-label">
{% trans %}Encoding{% endtrans %}</label>
......
......@@ -44,7 +44,7 @@ class SetEncodingTest(WebCase):
self.getPage("/settings/" + repo + "/")
def _set_encoding(self, repo, encoding):
self.getPage("/ajax/set-encoding/" + repo + "/", method="POST",
self.getPage("/api/set-encoding/" + repo + "/", method="POST",
body={'new_encoding': encoding})
def test_check_encoding(self):
......
......@@ -34,6 +34,7 @@ from rdiffweb import i18n # @UnusedImport
from rdiffweb import rdw_config, page_main
from rdiffweb import rdw_plugin
from rdiffweb import rdw_templating
from rdiffweb.api import ApiPage
from rdiffweb.dispatch import static, empty
from rdiffweb.page_admin import AdminPage
from rdiffweb.page_browse import BrowsePage
......@@ -66,7 +67,7 @@ class Root(LocationsPage):
self.admin = AdminPage(app)
self.prefs = PreferencesPage(app)
self.settings = SettingsPage(app)
self.ajax = empty()
self.api = ApiPage(app)
# Register static dir.
static_dir = pkg_resources.resource_filename('rdiffweb', 'static') # @UndefinedVariable
......
......@@ -29,6 +29,7 @@ from builtins import str, delattr
import cherrypy
from cherrypy.test import helper
from future.utils import native_str
import json
import os
import pkg_resources
import shutil
......@@ -217,6 +218,11 @@ class WebCase(helper.CPWebCase):
headers.extend(self.cookies)
helper.CPWebCase.getPage(self, url, headers, method, body, protocol)
def getJson(self, *args, **kwargs):
self.getPage(*args, **kwargs)
self.assertStatus(200)
return json.loads(self.body.decode('utf8'))
def _login(self, username=USERNAME, password=PASSWORD):
self.getPage("/login/", method='POST', body={'login': username, 'password': password})
self.assertStatus('303 See Other')
......
#!/usr/bin/python
# -*- coding: utf-8 -*-
# rdiffweb, A web interface to rdiff-backup repositories
# Copyright (C) 2017 rdiffweb contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Created on Nov 16, 2017
@author: Patrik Dufresne
"""
from __future__ import unicode_literals
import logging
import unittest
from rdiffweb.test import WebCase
class APITest(WebCase):
login = True
reset_app = True
reset_testcases = True
@classmethod
def setup_server(cls):
WebCase.setup_server(enabled_plugins=['SQLite'])
def test_get_currentuser(self):
data = self.getJson('/api/currentuser/')
self.assertEqual(data.get('username'), 'admin')
self.assertEqual(data.get('is_admin'), True)
self.assertEqual(data.get('email'), '')
# This value change on every execution.
# self.assertEqual(data.get('user_root'), 'admin')
self.assertIn('/tmp/rdiffweb_tests', data.get('user_root'))
self.assertEqual(data.get('repos'), ['testcases/'])
def test_get_currentuser_repos(self):
data = self.getJson('/api/currentuser/repos')
self.assertEqual(data, ['testcases/'])
if __name__ == "__main__":
# import sys;sys.argv = ['', 'Test.testName']
logging.basicConfig(level=logging.DEBUG)
unittest.main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment