Skip to content

Commit

Permalink
Update 2024.10.12
Browse files Browse the repository at this point in the history
  • Loading branch information
nicozanf committed Oct 12, 2024
1 parent 9291feb commit 0d05f25
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 41 deletions.
46 changes: 44 additions & 2 deletions docs/chapter-05.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
19 changes: 4 additions & 15 deletions docs/chapter-06.rst
Original file line number Diff line number Diff line change
Expand Up @@ -345,13 +345,7 @@ and in the template:
.. code:: html
...
<div id="py4web-flash"></div>
...
<script src="js/utils.js"></script>
[[if globals().get('flash'):]]
<script>utils.flash([[=XML(flash)]]);</script>
[[pass]]
<flash-alerts class="padded" data-alert="[[=globals().get('flash','')]]"></flash-alerts>
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
Expand All @@ -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``,
Expand Down Expand Up @@ -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
Expand Down
86 changes: 69 additions & 17 deletions docs/chapter-13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:

Expand All @@ -214,14 +215,61 @@ 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
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
^^^^^^^^^^^^^^^^

Expand All @@ -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.
Expand Down Expand Up @@ -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'))
Expand Down
12 changes: 5 additions & 7 deletions docs/chapter-16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Binary file added docs/images/icon-gear.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/icon-lens.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/icon-start.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/icon-stop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0d05f25

Please sign in to comment.