Commits

Kirill Simonov committed 84ee259

Adapted tweak.shell to work with CSRF protection.

Comments (0)

Files changed (4)

src/htsql/core/fmt/html.py

         self.case = case
 
     def scan(self, stream):
-        input = stream.read()
+        if isinstance(stream, (str, unicode)):
+            input = stream
+        else:
+            input = stream.read()
         if isinstance(input, str):
             try:
                 input = input.decode('utf-8')

src/htsql/tweak/shell/command.py

 from ... import __version__, __legal__
 from ...core.util import maybe, listof
 from ...core.context import context
-from ...core.adapter import Adapter, adapt, call
-from ...core.error import HTTPError
+from ...core.adapter import Adapter, adapt, adapt_many, call
+from ...core.error import HTTPError, PermissionError
 from ...core.domain import (Domain, BooleanDomain, NumberDomain, DateTimeDomain,
                             ListDomain, RecordDomain)
-from ...core.cmd.command import UniversalCmd, Command
-from ...core.cmd.act import (Act, RenderAction, UnsupportedActionError,
-                             produce, safe_produce, analyze)
+from ...core.cmd.command import UniversalCmd, Command, DefaultCmd
+from ...core.cmd.act import (Act, Action, RenderAction, UnsupportedActionError,
+                             act, produce, safe_produce, analyze)
 from ...core.model import HomeNode, InvalidNode, InvalidArc
 from ...core.classify import classify, normalize
 from ...core.tr.error import TranslateError
+from ...core.tr.lookup import lookup_command
 from ...core.tr.syntax import (StringSyntax, NumberSyntax, SegmentSyntax,
-                               IdentifierSyntax)
+                               IdentifierSyntax, QuerySyntax)
+from ...core.tr.bind import bind
 from ...core.tr.binding import CommandBinding
 from ...core.tr.signature import Signature, Slot
 from ...core.tr.error import BindError
 from ...core.tr.fn.bind import BindCommand
 from ...core.fmt.json import (escape_json, dump_json, JS_SEQ, JS_MAP, JS_END,
                               to_raw, profile_to_raw)
+from ...core.fmt.html import Template
 from ..resource.locate import locate
 import re
 import cgi
         self.names = names
 
 
-class EvaluateCmd(Command):
+class ProduceCmd(Command):
 
-    def __init__(self, query, action=None, page=None):
+    def __init__(self, query, page=None):
         assert isinstance(query, unicode)
-        assert isinstance(action, maybe(unicode))
         assert isinstance(page, maybe(int))
+        if page is None:
+            page = 1
         self.query = query
-        self.action = action
         self.page = page
 
 
+class AnalyzeCmd(Command):
+
+    def __init__(self, query):
+        assert isinstance(query, unicode)
+        self.query = query
+
+
+class WithPermissionsCmd(Command):
+
+    def __init__(self, command, can_read, can_write):
+        assert isinstance(command, Command)
+        assert isinstance(can_read, bool)
+        assert isinstance(can_write, bool)
+        self.command = command
+        self.can_read = can_read
+        self.can_write = can_write
+
+
 class ShellSig(Signature):
 
     slots = [
     ]
 
 
-class EvaluateSig(Signature):
+class ProduceSig(Signature):
 
     slots = [
             Slot('query'),
-            Slot('action', is_mandatory=False),
             Slot('page', is_mandatory=False),
     ]
 
 
+class AnalyzeSig(Signature):
+
+    slots = [
+            Slot('query'),
+    ]
+
+
+class WithPermissionsSig(Signature):
+
+    slots = [
+            Slot('query'),
+            Slot('can_read'),
+            Slot('can_write'),
+    ]
+
+
 class BindShell(BindCommand):
 
     call('shell')
         return CommandBinding(self.state.scope, command, self.syntax)
 
 
-class BindEvaluate(BindCommand):
+class BindProduce(BindCommand):
 
-    call('evaluate')
-    signature = EvaluateSig
+    call('produce')
+    signature = ProduceSig
 
-    def expand(self, query, action, page):
+    def expand(self, query, page=None):
         if not isinstance(query, StringSyntax):
             raise BindError("a string literal is required", query.mark)
         query = query.value
-        if action is not None:
-            if not isinstance(action, StringSyntax):
-                raise BindError("a string literal is required", action.mark)
-            if action.value not in [u'produce', u'analyze']:
-                raise BindError("'produce' or 'analyze' is expected",
-                                action.mark)
-            action = action.value
         if page is not None:
             if not isinstance(page, NumberSyntax) and page.is_integer:
                 raise BindError("an integer literal is required", page.mark)
             page = int(page.value)
-        command = EvaluateCmd(query, action, page)
+        command = ProduceCmd(query, page)
+        return CommandBinding(self.state.scope, command, self.syntax)
+
+
+class BindAnalyze(BindCommand):
+
+    call('analyze')
+    signature = AnalyzeSig
+
+    def expand(self, query):
+        if not isinstance(query, StringSyntax):
+            raise BindError("a string literal is required", query.mark)
+        query = query.value
+        command = AnalyzeCmd(query)
+        return CommandBinding(self.state.scope, command, self.syntax)
+
+
+class BindWithPermissions(BindCommand):
+
+    call('with_permissions')
+    signature = WithPermissionsSig
+
+    def expand(self, query, can_read, can_write):
+        if not isinstance(query, SegmentSyntax):
+            raise BindError("a segment is required", query.mark)
+        query = QuerySyntax(query, query.mark)
+        literals = [can_read, can_write]
+        values = []
+        domain = BooleanDomain()
+        for literal in literals:
+            if not isinstance(literal, StringSyntax):
+                raise BindError("a string literal is required", literal.mark)
+            try:
+                value = domain.parse(literal.value)
+            except ValueError, exc:
+                raise BindError(str(exc), literal.mark)
+            values.append(value)
+        can_read, can_write = values
+        with context.env(can_read=context.env.can_read and can_read,
+                         can_write=context.env.can_write and can_write):
+            binding = bind(query)
+            command = lookup_command(binding)
+            if command is None:
+                command = DefaultCmd(binding)
+        command = WithPermissionsCmd(command, can_read, can_write)
         return CommandBinding(self.state.scope, command, self.syntax)
 
 
 
     def __call__(self):
         query = self.command.query
-        if query is not None:
-            query = query.encode('utf-8')
         resource = locate('/shell/index.html')
         assert resource is not None
-        database_name = context.app.htsql.db.database
-        htsql_version = __version__
-        htsql_legal = __legal__
+        database_name = context.app.htsql.db.database.decode('utf-8', 'replace')
+        htsql_version = __version__.decode('ascii')
+        htsql_legal = __legal__.decode('ascii')
         server_root = context.app.tweak.shell.server_root
         if server_root is None:
             server_root = wsgiref.util.application_uri(self.action.environ)
         if server_root.endswith('/'):
             server_root = server_root[:-1]
+        server_root = server_root.decode('utf-8')
         resource_root = (server_root + '/%s/shell/'
                          % context.app.tweak.resource.indicator)
-        if query is not None and query not in ['', '/']:
+        resource_root = resource_root.decode('utf-8')
+        if query is not None and query not in [u'', u'/']:
             query_on_start = query
-            evaluate_on_start = 'true'
+            evaluate_on_start = u'true'
         else:
-            query_on_start = '/'
-            evaluate_on_start = 'false'
-        implicit = str(self.command.is_implicit).lower()
-        data = resource.data
-        data = self.patch(data, 'base href', resource_root)
-        data = self.patch(data, 'data-database-name', database_name)
-        data = self.patch(data, 'data-htsql-version', htsql_version)
-        data = self.patch(data, 'data-htsql-legal', htsql_legal)
-        data = self.patch(data, 'data-server-root', server_root)
-        data = self.patch(data, 'data-query-on-start', query_on_start)
-        data = self.patch(data, 'data-evaluate-on-start', evaluate_on_start)
-        data = self.patch(data, 'data-implicit-shell', implicit)
+            query_on_start = u'/'
+            evaluate_on_start = u'false'
+        can_read_on_start = unicode(context.env.can_read).lower()
+        can_write_on_start = unicode(context.env.can_write).lower()
+        implicit_shell = unicode(self.command.is_implicit).lower()
         status = '200 OK'
         headers = [('Content-Type', 'text/html; charset=UTF-8')]
-        body = [data]
+        template = Template(resource.data)
+        body = template(resource_root=cgi.escape(resource_root, True),
+                        database_name=cgi.escape(database_name, True),
+                        htsql_version=cgi.escape(htsql_version, True),
+                        htsql_legal=cgi.escape(htsql_legal, True),
+                        server_root=cgi.escape(server_root, True),
+                        query_on_start=cgi.escape(query_on_start, True),
+                        evaluate_on_start=cgi.escape(evaluate_on_start, True),
+                        can_read_on_start=cgi.escape(can_read_on_start, True),
+                        can_write_on_start=cgi.escape(can_write_on_start, True),
+                        implicit_shell=cgi.escape(implicit_shell, True))
+        body = (chunk.encode('utf-8') for chunk in body)
         return (status, headers, body)
 
     def patch(self, data, prefix, value):
         yield JS_END
 
 
-class RenderEvaluate(Act):
+class RenderProduceAnalyze(Act):
 
-    adapt(EvaluateCmd, RenderAction)
+    adapt_many((ProduceCmd, RenderAction),
+               (AnalyzeCmd, RenderAction))
 
     def __call__(self):
         addon = context.app.tweak.shell
         command = UniversalCmd(self.command.query.encode('utf-8'))
         limit = None
         try:
-            if self.command.action == 'analyze':
+            if isinstance(self.command, AnalyzeCmd):
                 plan = analyze(command)
             else:
                 page = self.command.page
                     product = produce(command)
         except UnsupportedActionError, exc:
             body = self.render_unsupported(exc)
+        except PermissionError, exc:
+            body = self.render_permissions(exc)
         except HTTPError, exc:
             body = self.render_error(exc)
         else:
-            if self.command.action == 'analyze':
+            if isinstance(self.command, AnalyzeCmd):
                 body = self.render_sql(plan)
             else:
                 if product:
         yield u"unsupported"
         yield JS_END
 
+    def render_permissions(self, exc):
+        detail = exc.detail.decode('utf-8')
+        yield JS_MAP
+        yield u"type"
+        yield u"permissions"
+        yield u"detail"
+        yield detail
+        yield JS_END
+
     def render_error(self, exc):
         detail = exc.detail.decode('utf-8')
         hint = None
         yield JS_END
 
 
+class ActWithPermissions(Act):
+
+    adapt(WithPermissionsCmd, Action)
+
+    def __call__(self):
+        can_read = context.env.can_read and self.command.can_read
+        can_write = context.env.can_write and self.command.can_write
+        with context.env(can_read=can_read, can_write=can_write):
+            return act(self.command.command, self.action)
+
+

src/htsql/tweak/shell/static/index.html

 <html>
 
   <head>
-    <base href="./">
+    <base href="{{ resource_root }}">
     <title>HTSQL</title>
     <script type="text/javascript" src="external/jquery/jquery-1.6.4.min.js"></script>
     <link rel="stylesheet" href="external/codemirror/lib/codemirror.css">
     <script type="text/javascript" src="shell.js"></script>
   </head>
 
-  <body data-database-name="HTSQL"
-        data-htsql-version="2.0"
-        data-htsql-legal="Copyright (c) 2006-2012, Prometheus Research, LLC"
-        data-server-root="http://localhost:8080"
-        data-query-on-start="/"
-        data-evaluate-on-start="false"
-        data-implicit-shell="false">
+  <body data-database-name="{{ database_name }}"
+        data-htsql-version="{{ htsql_version }}"
+        data-htsql-legal="{{ htsql_legal }}"
+        data-server-root="{{ server_root }}"
+        data-query-on-start="{{ query_on_start }}"
+        data-evaluate-on-start="{{ evaluate_on_start }}"
+        data-can-read-on-start="{{ can_read_on_start }}"
+        data-can-write-on-start="{{ can_write_on_start }}"
+        data-implicit-shell="{{ implicit_shell }}">
 
     <div id="viewport">
       <div class="heading-area">

src/htsql/tweak/shell/static/shell.js

             serverRoot: $body.attr('data-server-root') || "",
             queryOnStart: $body.attr('data-query-on-start') || "/",
             evaluateOnStart: ($body.attr('data-evaluate-on-start') == 'true'),
+            canReadOnStart: ($body.attr('data-can-read-on-start') == 'true'),
+            canWriteOnStart: ($body.attr('data-can-write-on-start') == 'true'),
             implicitShell: ($body.attr('data-implicit-shell') == 'true')
         };
     }
                 query = "/shell('" + query.replace(/'/g,"''") + "')";
         }
         var url = config.serverRoot+encodeURI(query).replace(/#/, '%23');
-        if (replace)
-            history.replaceState(data, title, url);
-        else
-            history.pushState(data, title, url);
+        try {
+            if (replace)
+                history.replaceState(data, title, url);
+            else
+                history.pushState(data, title, url);
+        }
+        catch (e) {
+            log(e);
+        }
     }
 
-    function run(query, action, page) {
+    function run(query, action, page, with_permissions) {
         if (!query)
             return;
         if (!config.serverRoot)
         state.lastQuery = query;
         state.lastAction = action;
         state.lastPage = page;
+        var success = handleSuccess;
+        var failure = handleFailure;
         if (!action)
             action = 'produce';
-        if (!page)
-            page = 1;
-        query = "/evaluate('" + query.replace(/'/g,"''") + "'"
-                + ",'" + action + "'" + "," + page + ")";
+        if (page) {
+            query = "/" + action + "('" + query.replace(/'/g,"''") + "',"
+                    + page + ")";
+        }
+        else {
+            query = "/" + action + "('" + query.replace(/'/g,"''") + "')";
+        }
+        if (with_permissions && (!config.canReadOnStart || !config.canWriteOnStart)) {
+            query = "/with_permissions(" + query + ",'" + config.canReadOnStart
+                    + "','" + config.canWriteOnStart + "')";
+            success = handleSuccessWithPermissions;
+        }
         var url = config.serverRoot+encodeURI(query).replace(/#/, '%23');
         $.ajax({
             url: url,
             dataType: 'json',
-            success: handleSuccess,
-            error: handleFailure,
+            success: success,
+            error: failure,
         });
         state.waiting = true;
         setTimeout(showWaiting, 1000);
             case "error":
                 handleError(output);
                 break;
+            case "permissions":
+                handleError(output);
+                break;
             case "unsupported":
                 handleUnsupported(output);
         }
     }
 
+    function handleSuccessWithPermissions(output) {
+        if (output.type == "permissions") {
+            output.type = "empty";
+        }
+        handleSuccess(output);
+    }
+
     function handleError(output) {
         if (state.$panel)
             state.$panel.hide();
         return state.identifiers;
     }
 
+    function ajaxSend(evt, xhr, settings) {
+        var name = 'htsql-csrf-token';
+        var token = null;
+        if (document.cookie && document.cookie != '') {
+            var cookies = document.cookie.split(';');
+            for (var i = 0; i < cookies.length; i++) {
+                var cookie = $.trim(cookies[i]);
+                if (cookie.substring(0, name.length+1) == (name+'=')) {
+                    token = decodeURIComponent(cookie.substring(name.length+1));
+                    break;
+                }
+            }
+        }
+        if (token) {
+            xhr.setRequestHeader('X-HTSQL-CSRF-Token', token);
+        }
+    }
+
     var config = makeConfig();
     var environ = makeEnviron();
     var state = makeState();
     $(window).bind('popstate', popstateWindow);
     $('#help-popup').click(function (e) { e.stopPropagation(); });
 
+    $(document).ajaxSend(ajaxSend);
+
     $('#schema').hide();
     $('#close-sql').hide();
 
 
     pushHistory(config.queryOnStart, true);
     if (config.evaluateOnStart) {
-        run(config.queryOnStart);
+        run(config.queryOnStart, null, null, true);
     }
 
 });
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.