Commits

Tetsuya Morimoto  committed e2fb1a3

added oauth flow trace feature using seqdiag

  • Participants
  • Parent commits 2ef35c3

Comments (0)

Files changed (8)

     "Flask-WTF",
     "Flask-OAuth",
     "SQLAlchemy",
+    "seqdiag",
     "Sphinx",
     "WTForms",
 ]

File src/raido/consts.py

+# -*- coding: utf-8 -*-
+"""
+    raido.consts
+    ^^^^^^^^^^^^
+
+    Constant Module
+"""
+
+DIAG_DEFAULT_FONTS = [
+    "c:/windows/fonts/VL-Gothic-Regular.ttf",  # for Windows
+    "c:/windows/fonts/msmincho.ttf",  # for Windows
+    "/usr/share/fonts/truetype/ipafont/ipagp.ttf",  # for Debian
+    "/usr/local/share/font-ipa/ipagp.otf",  # for FreeBSD
+    "/System/Library/Fonts/AppleGothic.ttf",  # for MaxOS
+]
+DIAG_COLOR_SUCCESS = "#99CCFF"
+DIAG_COLOR_FAIL = "#FF9999"
+DIAG_OAUTH2_GROUP = 'browser; consumer; provider color="{0}"'
+
+OAUTH2_PROGRESS = {
+    "req_code": 'browser   -> provider [label = "Authorization Request"];\n',
+    "res_code": 'browser  <-- provider [label = "Authorization Code"];\n'\
+                'browser   -> consumer [label = "Redirected"];\n',
+    "chk_logged_in": 'provider  -> provider [label = "Verify logged in"];\n'\
+                     'provider <-- provider;\n',
+    "get_login_form": 'browser   -> provider [label = "GET Login Form"];\n',
+    "res_login_form": 'browser  <-- provider [label = "Login Form"];\n',
+    "req_login": 'browser   -> provider [label = "Login Request"];\n',
+    "res_logged_in": 'browser  <-- provider [label = "Logged in"];\n',
+    "redirect": 'browser  <-- provider [label = "Redirected"];\n',
+    "req_token": '  consumer  -> provider [label = "Access Token Request"];\n',
+    "res_token": '  consumer <-- provider [label = "Access Tokenest"];\n',
+}
+
+OAUTH2_SEQUENCE_DIAG = """
+{
+    fontsize = 11; // default
+
+    browser   -> consumer [label = "Login Request"];
+    browser  <-- consumer [label = "Redirect for request"];
+    browser   -> provider [label = "Authorization Request"];
+    provider  -> provider [label = "Verify logged in"];
+    provider <-- provider;
+    browser  <-- provider [label = "Authorization Code"];
+    browser   -> consumer [label = "Redirected"];
+      consumer  -> provider [label = "Access Token Request"];
+      consumer <-- provider [label = "Access Tokenest"];
+}
+"""

File src/raido/provider.py

 from flask import (
         g, jsonify, redirect, request, render_template, session, url_for)
 
+from raido.consts import OAUTH2_SEQUENCE_DIAG
 from raido.forms import LoginForm, ConsumerForm
 from raido.models import *
 from raido.utils.contextmanagers import (
 
 app.add_url_rule("/favicon.ico", "favicon",
         lambda: redirect(app.config["RAIDO_LOGO"]))
+app.debug_progress = []  # consider later for simultaneous access
 
 @app.route("/", methods=["GET", "POST"])
 def index():
     app.logger.debug("Session: {0}".format(session))
     form = LoginForm(request.form)
     if request.method == "POST" and form.validate():
+        app.debug_progress.append("req_login")
         if session.get("username") != form.username.data:
             logout()
         with get_object_or_none((User,), name=form.username.data) as u:
             db.session.commit()
         session["username"] = form.username.data
         app.logger.debug("{0} is logged in".format(u.name))
+        if form.next_url.data == url_for("index"):
+            app.debug_progress.append("res_logged_in")
+        else:
+            app.debug_progress.append("redirect")
         return redirect(form.next_url.data)
     # GET or others
+    app.debug_progress.append("get_login_form")
     form.next_url.data = request.args.get("next", url_for("index"))
+    app.debug_progress.append("res_login_form")
     return render_template("login.html", user=g.user, form=form)
 
 @app.route("/logout")
 
 @app.route("/oauth/2/auth_code", methods=["GET"])
 def oauth2_auth_code():
+    if app.debug_progress and app.debug_progress[-1] != "redirect":
+        app.debug_progress = []
+    app.debug_progress.append("req_code")
+    app.debug_progress.append("chk_logged_in")
     if not g.user:
         app.logger.debug("need login before getting authentication code")
+        app.debug_progress.append("redirect")
         return redirect(url_for("login", next=request.url))
 
     fields = ("client_id", "redirect_uri")
     db.session.commit()
 
     app.logger.debug("authentication code is created: {0}".format(code))
+    app.debug_progress.append("res_code")
     return redirect("{0}?code={1}".format(c.redirect_uri, code))
 
 @app.route("/oauth/2/access_token", methods=["GET", "POST"])
         app.logger.debug(msg)
         return valid, verified, rd_params
 
+    app.debug_progress.append("req_token")
     if request.method == "POST":
         code = request.form.get("code")
         request_dict = request.form
         if "next" in rd_params:
             result.update(next=rd_params["next"])
         app.logger.debug("access token is created: {0}".format(str(result)))
+    app.debug_progress.append("res_token")
     return jsonify(**result)
 
 @app.route("/info", methods=["GET"])
         return jsonify(id=t.user.id, name=t.user.name,
                        birth_date=_cnv(t.user.birth_date, "%Y-%m-%d"))
 
+@app.route("/debug", methods=["GET"])
+def debug():
+    cfg, root_path = app.config, app.root_path  # just for alias
+    # make whole flow
+    whole_path = cfg["OAUTH2_WHOLE_FLOW"] + "." + cfg["DIAG_FORMAT"]
+    whole_src = unicode(OAUTH2_SEQUENCE_DIAG, "utf-8")
+    make_flow_diagram(whole_path, root_path, cfg, whole_src)
+
+    # get/analysis your oauth flow result
+    path = cfg["DEBUG_OAUTH2_FLOW"] + "." + cfg["DIAG_FORMAT"]
+    source, result = generate_diag_source(app.debug_progress)
+    app.logger.debug("progress: {0}".format(app.debug_progress))
+    app.logger.debug("diag source:\n{0}".format(source.encode("utf-8")))
+    make_flow_diagram(path, root_path, cfg, source, clean=True)
+
+    # make sequence diagram
+    return render_template("debug.html", result=result,
+                            flow=path, whole_flow=whole_path)
+
 @app.before_request
 def before_request():
-    app.logger.debug("Session: {0}".format(session))
     g.user = None
     if "username" in session:
         g.user = User.query.filter_by(name=session["username"]).first()

File src/raido/settings.py

 
 # for web design
 RAIDO_LOGO = "http://www.logomaker.com/logo-images/c659737cccd24b25.gif"
+
+# for debug
+DIAG_FONT = []
+DIAG_FORMAT = "svg"
+DEBUG_OAUTH2_FLOW = "static/debug_oauth2_flow"
+OAUTH2_WHOLE_FLOW = "static/oauth2_whole_flow"

File src/raido/static/style.css

 body{
-background:#FFFFFF;
-margin:9px;
+background: #FFFFFF;
+margin: 9px;
 font: 8pt/14pt 'Lucida Grande', Verdana, Helvetica, sans-serif;
-color:#666666;
+color: #666666;
 }
 
 A:link{ color:#999999; text-decoration:none; }
 
 
 #wrap{
-width:90%;
-margin-left:auto;
-margin-right:auto;
+width: 90%;
+margin-left: auto;
+margin-right: auto;
 }
 
 #sidebar{
-float:left;
-text-align:left;
-width:150px;
+float: left;
+text-align: left;
+width: 150px;
 }
 
 #container{
-width:auto;
-margin-left:160px;
-border-left:#CCCCCC 1px solid;
+width: auto;
+margin-left: 160px;
+border-left: #CCCCCC 1px solid;
 }
 
 #content1{
-padding:20px;
-text-align:justify;
+padding: 20px;
+text-align: justify;
 }
 
 .content2{
-padding:0px 20px 10px;
-text-align:justify;
+padding: 0px 20px 10px;
+text-align: justify;
 }
 
 .flash{
-color:#FF0000;
-font-weight:bold;
+color: #FF0000;
+font-weight: bold;
 }
 
 h3{
-text-align:right;
-color:#6C8EFF;
-font-size:14pt;
-font-weight:bold;
+text-align: right;
+color: #6C8EFF;
+font-size: 14pt;
+font-weight: bold;
 }
 
 #title{
-height:120px;
-margin-top:30px;
-border-bottom:#CCCCCC 1px solid;
-text-align:right;
-font-size:10pt;
-letter-spacing:-1px;
-color:#CCCCCC;
+height: 120px;
+margin-top: 30px;
+border-bottom: #CCCCCC 1px solid;
+text-align: right;
+font-size: 10pt;
+letter-spacing: -1px;
+color: #CCCCCC;
 }
 
 #footer{
-text-align:center;
-font-size:10px;
-height:30px;
-margin-top:10px;
-border-top:#CCCCCC 1px solid;
-text-transform:lowercase;
+text-align: center;
+font-size: 10px;
+height: 30px;
+margin-top: 10px;
+border-top: #CCCCCC 1px solid;
+text-transform: lowercase;
 }
 
- #navlist
+#navlist
 {
 padding: 0 1px 1px;
 margin-left: 0;
 font: bold 12px Verdana, sans-serif;
-background:#F3F3F3;
+background: #F3F3F3;
 width: 13em;
 }
 
 {
 list-style: none;
 margin: 0;
-border-top:#CCCCCC 1px solid;
+border-top: #CCCCCC 1px solid;
 text-align: right;
 }
 
 background: #FFFFFF;
 }
 
+#oauth2_whole_flow
+{
+height: 80%;
+width: 80%;
+margin: 0;
+}

File src/raido/templates/base.html

         <li><a href="/login">Login</a></li>
         <li><a href="/consumer/list">Consumer List</a></li>
         <li><a href="/consumer/register">Consumer Register</a></li>
-        <li><a href="/info">OAuth 2 User Info</a></li>
+        <li><a href="/debug">Debug</a></li>
       </ul>
     </div>
   </div>  <!--end sidebar-->

File src/raido/templates/debug.html

+{% extends "base.html" %}
+{% block content1 %}
+<h3>Debug</h3>
+
+{% if result %}
+  <p>Your OAuth 2.0 flow is succeeded</p>
+{% else %}
+  <p class="flash">Your OAuth 2.0 flow is failed</p>
+{% endif %}
+
+<div>
+  <img src={{ flow }} alt="Your Flow" title="Your Flow" />
+</div>
+
+{% if not result %}
+  <strong>OAuth 2.0 Whole Flow</strong>
+  <div>
+    <img id="oauth2_whole_flow" src={{ whole_flow }}
+         alt="OAuth 2.0 Flow" title="OAuth 2.0 Flow" />
+  </div>
+{% endif %}
+{% endblock %}

File src/raido/views/helper.py

     View Helper Module
 """
 
+import os
 from operator import getitem
+from os.path import join as pathjoin
 from random import randint
 from flask import flash
 
             db.session.delete(obj)
             db.session.commit()
 
+def get_font_path(app_fonts):
+    from os.path import isfile
+    from raido.consts import DIAG_DEFAULT_FONTS
+    fonts = app_fonts + DIAG_DEFAULT_FONTS
+    exist_fonts = filter(isfile, fonts)
+    return exist_fonts[0] if exist_fonts[0:1] else None
+
+def generate_diag_source(progress):
+    from raido.consts import (DIAG_COLOR_SUCCESS, DIAG_COLOR_FAIL,
+            DIAG_OAUTH2_GROUP, OAUTH2_PROGRESS)
+    source, result = "", False
+    if progress:
+        result = progress[-1] == "res_token"
+        color = DIAG_COLOR_SUCCESS if result else DIAG_COLOR_FAIL
+        group = DIAG_OAUTH2_GROUP.format(color)
+        source = "{\n"
+        source += "group {" + group + "}\n"
+        source += "".join(OAUTH2_PROGRESS[prog] for prog in progress)
+        source += "}"
+    return unicode(source, "utf-8"), result
+
+def generate_diagram(app_config, source, path):
+    from seqdiag import diagparser, builder, DiagramDraw
+    if not source:
+        return
+    fmt = app_config["DIAG_FORMAT"]
+    font = get_font_path(app_config["DIAG_FONT"])
+    try:
+        tree = diagparser.parse_string(source)
+        diagram = builder.ScreenNodeBuilder.build(tree)
+        draw = DiagramDraw.DiagramDraw(fmt, diagram, font=font, filename=path)
+        draw.draw()
+        draw.save()
+    except Exception as err:
+        flash(repr(err))
+
+def make_flow_diagram(path, root_path, cfg, source, clean=False):
+    abspath = pathjoin(root_path, path)
+    if clean and os.access(abspath, os.F_OK):
+        os.remove(abspath)
+    if not os.access(abspath, os.F_OK):
+        generate_diagram(cfg, source, abspath)
+
 def flash_form_errors(form):
     for field in form.errors:
         flash("{0}: {1}".format(field, ", ".join(form.errors[field])))