diff --git a/docs/chapter-05.rst b/docs/chapter-05.rst index 2bbdbce..3ce7d52 100644 --- a/docs/chapter-05.rst +++ b/docs/chapter-05.rst @@ -333,8 +333,8 @@ The scaffold app contains an example of a more complex action: Notice the following: -- ``request``, ``response``, ``abort`` are defined by - which is a fast bottlepy spin-off. +- ``request``, ``response``, ``abort`` are defined by ``ombott`` + which is a minimal and fast bottlepy spin-off. - ``redirect`` and ``URL`` are similar to their web2py counterparts - helpers (``A``, ``DIV``, ``SPAN``, ``IMG``, etc) must be imported from ``yatl.helpers`` . They work pretty much as in web2py @@ -432,3 +432,45 @@ relative to an app. Python files (i.e. "\*.py") in a list passed to the decorator are ignored since they are watched by default. Handler function’s parameter is a list of filepaths that were changed. All exceptions inside handlers are printed in terminal. + +Domain-mapped apps +------------------ + +In production environments it is often required to have several apps being +served by a single py4web server, where different apps are mapped to +different domains. + +py4web can easily handle running multiple apps, but there is no build-in +mechanism for mapping domains to specific applications. Such mapping needs +to be done externally to py4web -- for instance using a web reverse-proxy, +such as nginx. + +While nginx or other reverse-proxies are also useful in production +environments for handling SSL termination, caching and other uses, +we cover only the mapping of domains to py4web applications here. + +An example nginx configuration for an application ``myapp`` mapped to +a domain ``myapp.example.com`` might look like that: + +.. code:: console + + server { + listen 80; + server_name myapp.example.com; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-PY4WEB-APPNAME /myapp; + location / { + proxy_pass http://127.0.0.1:8000/myapp$request_uri; + } + } + +This is an example ``server`` block of nginx configuraiton. One would have to create a separate such block for **each app/each domain** being served by py4web server. Note some important aspects: + +- ``server_name`` defines the domain mapped to the app ``myapp``, +- ``proxy_http_version 1.1;`` directive is optional, but highly recommended (otherwise nginx uses HTTP 1.0 to talk to the backend-server -- here py4web -- and it creates all kinds of issues with buffering and otherwise), +- ``proxy_set_header Host $host;`` directive ensures that the correct ``Host`` is passed to py4web -- here ``myapp.example.com`` +- ``proxy_set_header X-PY4WEB-APPNAME /myapp;`` directive ensures that py4web (and ombott) knows which app to serve and **also** that this application is domain-mapped -- pay specific attention to the slash (``/``) in front of the ``myapp`` name -- it is **required** to ensure correct parsing of URLs on ombott level, +- finally ``proxy_pass http://127.0.0.1:8000/myapp$request_uri;`` ensures that the request is passed in its entirity (``$request_uri``) to py4web server (here: ``127.0.0.1:8000``) and the correct app (``/myapp``). + +Such configuration ensures that all URL manipulation inside ombott and py4web - especially in modules such as ``Auth``, ``Form``, and ``Grid`` are done correctly using the domain to which the app is mapped to. diff --git a/docs/chapter-06.rst b/docs/chapter-06.rst index 30ea0b7..c4f5687 100644 --- a/docs/chapter-06.rst +++ b/docs/chapter-06.rst @@ -345,13 +345,7 @@ and in the template: .. code:: html - ... -
- ... - - [[if globals().get('flash'):]] - - [[pass]] + By setting the value of the message in the flash helper, a flash variable is returned by the action and this triggers the JS in the @@ -369,7 +363,7 @@ The client can also set/add flash messages by calling: :: - utils.flash({'message': 'hello world', 'class': 'info'}); + Q.flash({'message': 'hello world', 'class': 'info'}); py4web defaults to an alert class called ``info`` and most CSS frameworks define classes for alerts called ``success``, ``error``, @@ -657,14 +651,9 @@ require that users action have specific group membership: groups = Tags(db.auth_user) - def requires_membership(group_name): - return Condition( - lambda: group_name in groups.get(auth.user_id), - exception=HTTP(404) - ) - @action("payroll") - @action.uses(auth, requires_membership("employees")) + @action.uses(auth, + Condition(lambda: 'employees' in groups.get(auth.user_id), on_false=lambda: redirect('index'))) def payroll(): return diff --git a/docs/chapter-13.rst b/docs/chapter-13.rst index 64441a9..2322f35 100644 --- a/docs/chapter-13.rst +++ b/docs/chapter-13.rst @@ -159,19 +159,20 @@ username and password, and two factor authentication is enabled for the user, th There are a few Auth settings available to control how two factor authentication works. -The follow can be specified on Auth instantiation: +The following can be specified on Auth instantiation: -- two_factor_required -- two_factor_send +- ``two_factor_required`` +- ``two_factor_send`` +- ``two_factor_validate`` two_factor_required ^^^^^^^^^^^^^^^^^^^ -When you pass a method name to the two_factor_filter parameter you are telling py4web to call that method to determine whether or not this login should -be use or bypass two factor authentication. If your method returns True, then this login requires two factor. If it returns False, -two factor authentication is bypassed for this login. +When you pass a method name to the ``two_factor_required`` parameter you are telling py4web to call that method to determine whether or not this login should +be use or bypass two factor authentication. If your method returns True, then this login requires two factor. If it returns False, two factor authentication +is bypassed for this login. -Sample two_factor_filter method +Sample ``two_factor_required`` method This example shows how to allow users that are on a specific network. @@ -195,8 +196,8 @@ This example shows how to allow users that are on a specific network. two_factor_send ^^^^^^^^^^^^^^^ -When two factor authentication is active, py4web generates a 6 digit code (using random.randint) and sends it to you. How this code is sent, is up to you. -The two_factor_send argument to the Auth class allows you to specify the method that sends the two factor code to the user. +When two factor authentication is active, py4web can generate a 6 digit code (using random.randint) and makes it possible to send it to the user. How this code is +sent, is up to you. The ``two_factor_send`` argument to the Auth class allows you to specify the method that sends the two factor code to the user. This example shows how to send an email with the two factor code: @@ -214,7 +215,7 @@ This example shows how to send an email with the two factor code: print(e) return code -Notice that this method takes to arguments: the current user, and the code to be sent. +Notice that this method takes two arguments: the current user, and the code to be sent. Also notice this method can override the code and return a new one. .. code:: python @@ -222,6 +223,53 @@ Also notice this method can override the code and return a new one. auth.param.two_factor_required = user_outside_network auth.param.two_factor_send = send_two_factor_email +two_factor_validate +^^^^^^^^^^^^^^^^^^^ + +By default, py4web will validate the user input in the two factor form by comparing the code entered by the user with the code generated and sent using +``two_factor_send``. However, sometimes it may be useful to define a custom validation of this user-entered code. For instance, if one would like to use the +TOTP (or the Time-Based One-Time-Passwords) as the two factor authentication method, the validation requires comparing the code entered by the user with the +value generated at the same time at the server side. Hence, it is not sufficient to generate that value earlier when showing the form (using for instance +``two_factor_send`` method), because by the time the user submits the form, the current valid value may already be different. Instead, this value should be +generated when validating the form submitted by the user. + +To accomplish such custom validation, the ``two_factor_validate`` method is available. It takes two arguments - the current user and the code that was entered +by the user into the two factor authentication form. The primary use-case for this method is validation of time-based passwords. + +This example shows how to validate a time-based two factor code: + +.. code:: python + + def validate_code(user, code): + try: + # get the correct code from an external function + correct_code = generate_time_based_code(user_id) + except Exception as e: + # return None to indicate that validation could not be performed + return None + + # compare the value entered in the auth form with the correct code + if code == correct_code: + return True + else: + return False + +The ``validate_code`` method must return one of three values: + +- ``True`` - if the validation succeded, +- ``False`` - if the validation failed, +- ``None`` - if the validation was not possible for any reason + +Notice that - if defined - this method is _always_ called to validate the two factor authentication form. It is up to you to decide what kind of validation it +does. If the returned value is ``True``, the user input will be accepted as valid. If the returned value is ``False`` then the user input will be rejected as +invalid, number of tries will be decresed by one, and user will be asked to try again. If the returned value is ``None`` the user input will be checked against +the code generated with the use of ``two_factor_send`` method and the final result will depend on that comparison. In this case authentication will fail if ``two_factor_send`` +method was not defined, and hence no code was sent to the user. + +.. code:: python + + auth.param.two_factor_validate = validate_code + two_factor_tries ^^^^^^^^^^^^^^^^ @@ -236,9 +284,11 @@ Once this is all setup, the flow for two factor authentication is: - present the login page - upon successful login and user passes two_factor_required - redirect to py4web auth/two_factor endpoint - - generate 6 digit verification code - - call two_factor_send to send the verification code to the user + - if ``two_factor_send`` method has been defined: + - generate 6 digit verification code + - call ``two_factor_send`` to send the verification code to the user - display verification page where user can enter their code + - if ``two_factor_validate`` method has been defined - call it to validate the user-entered code - upon successful verification, take user to _next_url that was passed to the login page Important! If you filtered ``ALLOWED_ACTIONS`` in your app, make sure to whitelist the "two_factor" action so not to block the two factor API. @@ -444,11 +494,13 @@ enables the following syntax: groups = Tags(db.auth_user) - def requires_membership(group_name): - return Condition( - lambda: group_name in groups.get(auth.user_id), - exception=HTTP(404) - ) + class requires_membership(Fixture): + def __init__(self, group): + self.__prerequisites__ = [auth.user] # you must have a user before you can check + self.group = group # store the group when action defined + def on_request(self, context): # will be called if the action is called + if self.group not in groups.get(auth.user_id): + raise HTTP(401) # check and do something @action('index') @action.uses(requires_membership('teacher')) diff --git a/docs/chapter-16.rst b/docs/chapter-16.rst index 58b61f1..c146181 100644 --- a/docs/chapter-16.rst +++ b/docs/chapter-16.rst @@ -77,12 +77,10 @@ To prevent database locks (in particular with sqlite) we recommend: Sending messages using a background task ---------------------------------------- -As en example of application of the above, -Consider the case of wanting to send emails asynchronously from a background task. -In this example we send them using SendGrid from Twilio (https://www.twilio.com/docs/sendgrid/for-developers/sending-email/quickstart-python) -Also we assume emails are represented by the following JSON structure +As en example of application of the above, consider the case of wanting to send emails asynchronously from a background task. +In this example we send them using SendGrid from Twilio (https://www.twilio.com/docs/sendgrid/for-developers/sending-email/quickstart-python). -That means you need a new task: +Here is a possible scheduler task to send the email: .. code:: python @@ -119,8 +117,8 @@ To schedule sending a new email do: scheduler.enqueue_run(name="sendmail", inputs=email, scheduled_for=None) The key:value in the email representation must match the arguments of the task. -The ``scheuled_for`` argument is optional and allows you to specify when the email should be sent. -You can use the Dashboard to see the status of your ``task_run``s for the task called ``sendmail``. +The ``scheduled_for`` argument is optional and allows you to specify when the email should be sent. +You can use the Dashboard to see the status of your ``task_run``\s for the task called ``sendmail``. You can also tell auth to tap into above mechanism for sending emails: diff --git a/docs/images/icon-gear.png b/docs/images/icon-gear.png new file mode 100644 index 0000000..a6bcc60 Binary files /dev/null and b/docs/images/icon-gear.png differ diff --git a/docs/images/icon-lens.png b/docs/images/icon-lens.png new file mode 100644 index 0000000..632f151 Binary files /dev/null and b/docs/images/icon-lens.png differ diff --git a/docs/images/icon-start.png b/docs/images/icon-start.png new file mode 100644 index 0000000..426638c Binary files /dev/null and b/docs/images/icon-start.png differ diff --git a/docs/images/icon-stop.png b/docs/images/icon-stop.png new file mode 100644 index 0000000..9148690 Binary files /dev/null and b/docs/images/icon-stop.png differ