Ejemplo n.º 1
0
    def device_login(self, path=None):
        """
        REDIRECT BROWSER TO AUTH0 LOGIN
        """
        state = request.args.get("state")
        self.session_manager.setup_session(session)
        session.code_verifier = bytes2base64URL(Random.bytes(32))
        code_challenge = bytes2base64URL(
            sha256(session.code_verifier.encode("utf8")))

        query = Data(
            client_id=self.device.auth0.client_id,
            redirect_uri=self.device.auth0.redirect_uri,
            state=state,
            nonce=bytes2base64URL(Random.bytes(32)),
            code_challenge=code_challenge,
            response_type="code",
            code_challenge_method="S256",
            response_mode="query",
            audience=self.device.auth0.audience,
            scope=self.device.auth0.scope,
        )
        url = str(
            URL("https://" + self.device.auth0.domain + "/authorize",
                query=query))

        Log.note("Forward browser to {{url}}", url=url)
        return redirect(url, code=302)
Ejemplo n.º 2
0
    def get_subscriber(self, name=None):
        """
        GET SUBSCRIBER BY id, OR BY QUEUE name
        """
        with self.db.transaction() as t:
            result = t.query(
                SQL(
                    f"""
                    SELECT
                        MIN(s.id) as id
                    FROM
                        {SUBSCRIBER} AS s
                    LEFT JOIN 
                        {QUEUE} as q on q.id = s.queue
                    WHERE
                        q.name = {quote_value(name)}
                    GROUP BY 
                        s.queue
                    """
                )
            )
            if not result:
                Log.error("not expected")

            queue = self.get_or_create_queue(name)
            sub_info = t.query(
                sql_query(
                    {"from": SUBSCRIBER, "where": {"eq": {"id": first_row(result).id}}}
                )
            )

            return Subscription(queue=queue, kwargs=first_row(sub_info))
Ejemplo n.º 3
0
    def login(self, path=None):
        """
        EXPECT AN ACCESS TOKEN, RETURN A SESSION TOKEN
        """
        now = Date.now().unix
        try:
            access_token = get_token_auth_header()
            # if access_token.error:
            #     Log.error("{{error}}: {{error_description}}", access_token)
            if len(access_token.split(".")) == 3:
                access_details = self.verify_jwt_token(access_token)
                session.scope = access_details["scope"]

            # ADD TO SESSION
            self.session_manager.setup_session(session)
            user_details = self.verify_opaque_token(access_token)
            session.user = self.permissions.get_or_create_user(user_details)
            session.last_used = now

            self.markup_user()

            return Response(value2json(
                self.session_manager.make_cookie(session)),
                            status=200)
        except Exception as e:
            session.user = None
            session.last_used = None
            Log.error("failure to authorize", cause=e)
Ejemplo n.º 4
0
    def verify_jwt_token(self, token):
        jwks = http.get_json("https://" + self.auth0.domain +
                             "/.well-known/jwks.json")
        unverified_header = jwt.get_unverified_header(token)
        algorithm = unverified_header["alg"]
        if algorithm != "RS256":
            Log.error("Expecting a RS256 signed JWT Access Token")

        key_id = unverified_header["kid"]
        key = unwrap(first(key for key in jwks["keys"]
                           if key["kid"] == key_id))
        if not key:
            Log.error("could not find {{key}}", key=key_id)

        try:
            return jwt.decode(
                token,
                key,
                algorithms=algorithm,
                audience=self.auth0.api.identifier,
                issuer="https://" + self.auth0.domain + "/",
            )
        except jwt.ExpiredSignatureError as e:
            Log.error("Token has expired", code=403, cause=e)
        except jwt.JWTClaimsError as e:
            Log.error(
                "Incorrect claims, please check the audience and issuer",
                code=403,
                cause=e,
            )
        except Exception as e:
            Log.error("Problem parsing", cause=e)
Ejemplo n.º 5
0
    def output(*args, **kwargs):
        # IS THIS A NEW SESSION
        now = Date.now().unix
        user = session.get("user")
        if not user:
            Log.error("must authorize first")

        session.last_used = now
        return func(*args, user=user, **kwargs)
Ejemplo n.º 6
0
def get_token_auth_header():
    """Obtains the Access Token from the Authorization Header
    """
    try:
        auth = request.headers.get("Authorization", None)
        bearer, token = auth.split()
        if bearer.lower() == "bearer":
            return token
    except Exception as e:
        pass
    Log.error('Expecting "Authorization = Bearer <token>" in header')
Ejemplo n.º 7
0
def private_scoped(user):
    """A valid access token and an appropriate scope are required to access this route
    """
    if requires_scope(config.auth0.scope):
        response = (
            "Hello from a private endpoint! You need to be authenticated and have a scope of "
            + config.auth0.scope
            + " to see this."
        )
        return jsonify(message=response)
    Log.error("You don't have access to {{scope}}", scope=config.auth0.scope, code=403)
Ejemplo n.º 8
0
 def verify_opaque_token(self, token):
     # Opaque Access Token
     url = "https://" + self.auth0.domain + "/userinfo"
     response = http.get_json(url,
                              headers={"Authorization": "Bearer " + token})
     DEBUG and Log.note("content: {{body|json}}", body=response)
     return response
Ejemplo n.º 9
0
def annotation():
    if flask.request.headers.get("content-length", "") in ["", "0"]:
        # ASSUME A BROWSER HIT THIS POINT, SEND text/html RESPONSE BACK
        return Response(ERROR_CONTENT,
                        status=400,
                        headers={"Content-Type": "text/html"})
    elif int(flask.request.headers["content-length"]) > QUERY_SIZE_LIMIT:
        Log.error("Query is too large to parse")

    request_body = flask.request.get_data().strip()
    text = utf82unicode(request_body)
    data = json2value(text)

    try:
        record_request(flask.request, data, None, None)
    except Exception as e:
        Log.error("Problem processing request {{request}}")
Ejemplo n.º 10
0
    def __init__(self,
                 flask_app,
                 auth0,
                 permissions,
                 session_manager,
                 device=None):
        if not auth0.domain:
            Log.error("expecting auth0 configuration")

        self.auth0 = auth0
        self.permissions = permissions
        self.session_manager = session_manager

        # ATTACH ENDPOINTS TO FLASK APP
        endpoints = auth0.endpoints
        if not endpoints.login or not endpoints.logout or not endpoints.keep_alive:
            Log.error("Expecting paths for login, logout and keep_alive")

        add_flask_rule(flask_app, endpoints.login, self.login)
        add_flask_rule(flask_app, endpoints.logout, self.logout)
        add_flask_rule(flask_app, endpoints.keep_alive, self.keep_alive)

        if device:
            self.device = device
            db = self.device.db = Sqlite(device.db)
            if not db.about("device"):
                with db.transaction() as t:
                    t.execute(
                        sql_create(
                            "device",
                            {
                                "state": "TEXT PRIMARY KEY",
                                "session_id": "TEXT"
                            },
                        ))
            if device.auth0.redirect_uri != text_type(
                    URL(device.home, path=device.endpoints.callback)):
                Log.error(
                    "expecting home+endpoints.callback == auth0.redirect_uri")

            add_flask_rule(flask_app, device.endpoints.register,
                           self.device_register)
            add_flask_rule(flask_app, device.endpoints.status,
                           self.device_status)
            add_flask_rule(flask_app, device.endpoints.login,
                           self.device_login)
            add_flask_rule(flask_app, device.endpoints.callback,
                           self.device_callback)
Ejemplo n.º 11
0
        Log.error("Query is too large to parse")

    request_body = flask.request.get_data().strip()
    text = utf82unicode(request_body)
    data = json2value(text)

    try:
        record_request(flask.request, data, None, None)
    except Exception as e:
        Log.error("Problem processing request {{request}}")


if __name__ == "__main__":
    CONFIG = startup.read_settings()
    constants.set(CONFIG.constants)
    Log.start(CONFIG.debug)

    app = Flask(__name__,
                static_url_path="/public",
                static_folder="./public",
                root_path=".")
    app.secret_key = CONFIG.annotation.auth0.client.secret
    app.debug = True

    requires_auth, login, logout, callback = oauth.setup(
        app, CONFIG.annotation.auth0)

    app.add_url_rule("/", None, requires_auth(home))
    app.add_url_rule("/dashboard", None, requires_auth(dashboard))
    app.add_url_rule("/annotation", None, requires_auth(annotation))
Ejemplo n.º 12
0
 def logout(self, path=None):
     if not session.session_id:
         Log.error("Expecting a sesison token")
     session.user = None
     session.last_used = None
     return Response(status=200)
Ejemplo n.º 13
0
@verify_user
def private_scoped(user):
    """A valid access token and an appropriate scope are required to access this route
    """
    if requires_scope(config.auth0.scope):
        response = (
            "Hello from a private endpoint! You need to be authenticated and have a scope of "
            + config.auth0.scope
            + " to see this."
        )
        return jsonify(message=response)
    Log.error("You don't have access to {{scope}}", scope=config.auth0.scope, code=403)


add_flask_rule(APP, "/api/public", public)
add_flask_rule(APP, "/api/private", private)
add_flask_rule(APP, "/api/private-scoped", private_scoped)


config = startup.read_settings()
constants.set(config.constants)
Log.start(config.debug)

session_manager = setup_flask_session(APP, config.session)
perm = Permissions(Sqlite(config.permissions.store))
auth = Authenticator(APP, config.auth0, perm, session_manager)

Log.note("start servers")
setup_flask_ssl(APP, config.flask)
APP.run(**config.flask)
Ejemplo n.º 14
0
 def keep_alive(self, path=None):
     if not session.session_id:
         Log.error("Expecting a sesison token")
     now = Date.now().unix
     session.last_used = now
     return Response(status=200)
Ejemplo n.º 15
0
def handle_auth_error(ex):
    ex = Except.wrap(ex)
    code = coalesce(ex.params.code, 401)
    Log.warning("sending error to client\n{{error}}", {"error": ex})
    return Response(value2json(ex), status=code)
Ejemplo n.º 16
0
 def delete_queue(self, name):
     Log.error("You do not need to do this")
Ejemplo n.º 17
0
    def device_callback(self, path=None):
        # HANDLE BROWESR RETURN FROM AUTH0 LOGIN
        error = request.args.get("error")
        if error:
            Log.error("You did it wrong")

        code = request.args.get("code")
        state = request.args.get("state")
        referer = request.headers.get("Referer")
        result = self.device.db.query(
            sql_query({
                "from": "device",
                "select": "session_id",
                "where": {
                    "eq": {
                        "state": state
                    }
                },
            }))
        if not result.data:
            Log.error("expecting valid state")
        device_session_id = result.data[0][0]

        # GO BACK TO AUTH0 TO GET TOKENS
        token_request = {
            "client_id": self.device.auth0.client_id,
            "redirect_uri": self.device.auth0.redirect_uri,
            "code_verifier": session.code_verifier,
            "code": code,
            "grant_type": "authorization_code",
        }
        DEBUG and Log.note("Send token request to Auth0:\n {{request}}",
                           request=token_request)
        auth_response = requests.request(
            "POST",
            str(URL("https://" + self.device.auth0.domain,
                    path="oauth/token")),
            headers={
                "Accept": "application/json",
                "Content-Type": "application/json",
                # "Referer": str(URL(self.device.auth0.redirect_uri, query={"code": code, "state": state})),
            },
            data=value2json(token_request),
        )

        try:
            auth_result = wrap(auth_response.json())
        except Exception as e:
            Log.error("not json {{value}}",
                      value=auth_response.content,
                      cause=e)

        # VERIFY TOKENS, ADD USER TO DEVICE'S SESSION
        user_details = self.verify_opaque_token(auth_result.access_token)
        self.session_manager.update_session(
            device_session_id,
            {"user": self.permissions.get_or_create_user(user_details)},
        )

        # REMOVE DEVICE SETUP STATE
        with self.device.db.transaction() as t:
            t.execute(SQL_DELETE + SQL_FROM + quote_column(self.device.table) +
                      SQL_WHERE + sql_eq(state=state))
        Log.note("login complete")
        return Response("Login complete. You may close this page", status=200)
Ejemplo n.º 18
0
    def clean(self):
        """
        REMOVE ANY RECORDS THAT ARE NOT NEEDED BY QUEUE OR SUBSCRIBERS
        """
        now = Date.now()

        # ANY BLOCKS TO FLUSH?
        with self.db.transaction() as t:
            result = t.query(
                SQL(
                    f"""
                    SELECT id, block_size_mb, block_start
                    FROM {QUEUE} AS q
                    JOIN {BLOCKS} AS b ON b.queue=q.id AND b.serial=q.block_start
                    WHERE b.last_used < {quote_value(now-Duration(WRITE_INTERVAL))}            
                    """
                )
            )

        for stale in rows(result):
            queue = first(q for q in self.queues if q.id == stale.id)
            queue._flush(**stale)

        # REMOVE UNREACHABLE MESSAGES
        conditions = []
        for q in self.queues:
            conditions.append(
                SQL(f"(queue = {quote_value(q.id)} AND serial IN (")
                + SQL(
                    f"""
                    SELECT m.serial
                    FROM {MESSAGES} AS m 
                    LEFT JOIN {UNCONFIRMED} as u ON u.serial = m.serial
                    LEFT JOIN {SUBSCRIBER} as s  ON s.queue = m.queue and s.id = u.subscriber 
                    LEFT JOIN {QUEUE} as q ON q.id = m.queue and m.serial >= q.block_start
                    LEFT JOIN {SUBSCRIBER} as la ON 
                        la.queue = m.queue AND 
                        la.last_confirmed_serial < m.serial AND 
                        m.serial < la.next_emit_serial+la.look_ahead_serial
                    WHERE 
                        m.queue = {q.id} AND
                        s.id IS NULL AND -- STILL UNCONFIRMED POP
                        q.id IS NULL AND -- NOT WRITTEN TO S3 YET
                        la.id IS NULL    -- NOT IN LOOK-AHEAD FOR SUBSCRIBER           
                    """
                )
                + SQL("))")
            )

        with self.db.transaction() as t:
            if DEBUG:
                result = t.query(
                    ConcatSQL(
                        SQL(f"SELECT count(1) AS `count` FROM {MESSAGES} WHERE "),
                        JoinSQL(SQL_OR, conditions),
                    )
                )
                Log.note(
                    "Delete {{num}} messages from database", num=first_row(result).count
                )

            t.execute(
                ConcatSQL(
                    SQL(f"DELETE FROM {MESSAGES} WHERE "), JoinSQL(SQL_OR, conditions)
                )
            )