Sorry, I see signing the email scrambled the patch:

> diff --git a/applications/welcome/controllers/default.py
> b/applications/welcome/controllers/default.py
> index c775603..e99ef0f 100644
> --- a/applications/welcome/controllers/default.py
> +++ b/applications/welcome/controllers/default.py
> @@ -57,4 +57,10 @@ def call():
>      """
>      return service()
>  
> +@auth.requires_membership('masters')
> +def master_zone():
> +    return dict(a='Hello, Master', b=request.vars.message)
>  
> +def get_master_code():
> +    url = auth.get_authorization_url(a='welcome', c='default',
> f='master_zone', vars=dict(message='Greetings'))
> +    return dict(a = A('copy this link', _href=url))
> diff --git a/applications/welcome/languages/es.py
> b/applications/welcome/languages/es.py
> index 7579cc3..54a57c6 100644
> --- a/applications/welcome/languages/es.py
> +++ b/applications/welcome/languages/es.py
> @@ -55,6 +55,7 @@
>  'Available Databases and Tables': 'Bases de datos y tablas disponibles',
>  'Back': 'Atrás',
>  'Buy this book': 'Compra este libro',
> +"Buy web2py's book": "Buy web2py's book",
>  'Cache': 'Caché',
>  'cache': 'caché',
>  'Cache Keys': 'Llaves de la Caché',
> @@ -83,6 +84,7 @@
>  'compile': 'compilar',
>  'compiled application removed': 'aplicación compilada eliminada',
>  'Components and Plugins': 'Componentes y Plugins',
> +'Config.ini': 'Config.ini',
>  'contains': 'contiene',
>  'Controller': 'Controlador',
>  'Controllers': 'Controladores',
> @@ -119,6 +121,7 @@
>  'Description': 'Descripción',
>  'design': 'diseño',
>  'DESIGN': 'DISEÑO',
> +'Design': 'Design',
>  'Design for': 'Diseño por',
>  'detecting': 'detectando',
>  'DISK': 'DISCO',
> @@ -145,6 +148,7 @@
>  'End of impersonation': 'Fin de suplantación',
>  'enter a number between %(min)g and %(max)g': 'introduzca un número
> entre %(min)g y %(max)g',
>  'enter a value': 'introduzca un valor',
> +'Enter an integer between %(min)g and %(max)g': 'Enter an integer
> between %(min)g and %(max)g',
>  'enter an integer between %(min)g and %(max)g': 'introduzca un entero
> entre %(min)g y %(max)g',
>  'enter date and time as %(format)s': 'introduzca fecha y hora como
> %(format)s',
>  'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"',
> @@ -177,6 +181,7 @@
>  'Groups': 'Grupos',
>  'Hello World': 'Hola Mundo',
>  'help': 'ayuda',
> +'Helping web2py': 'Helping web2py',
>  'Home': 'Inicio',
>  'How did you get here?': '¿Cómo llegaste aquí?',
>  'htmledit': 'htmledit',
> @@ -217,6 +222,7 @@
>  'License for': 'Licencia para',
>  'Live Chat': 'Chat en vivo',
>  'loading...': 'cargando...',
> +'Log In': 'Log In',
>  'Logged in': 'Sesión iniciada',
>  'Logged out': 'Sesión finalizada',
>  'Login': 'Inicio de sesión',
> @@ -256,6 +262,7 @@
>  'not in': 'no en',
>  'Object or table name': 'Nombre del objeto o tabla',
>  'Old password': 'Contraseña vieja',
> +'Online book': 'Online book',
>  'Online examples': 'Ejemplos en línea',
>  'Or': 'O',
>  'or import from csv file': 'o importar desde archivo CSV',
> @@ -319,6 +326,7 @@
>  'Services': 'Servicios',
>  'session expired': 'sesión expirada',
>  'shell': 'terminal',
> +'Sign Up': 'Sign Up',
>  'site': 'sitio',
>  'Size of cache:': 'Tamaño de la Caché:',
>  'some files could not be removed': 'algunos archivos no pudieron ser
> removidos',
> diff --git a/applications/welcome/models/db.py
> b/applications/welcome/models/db.py
> index 606dc6f..3438621 100644
> --- a/applications/welcome/models/db.py
> +++ b/applications/welcome/models/db.py
> @@ -8,6 +8,7 @@
>  ## if SSL/HTTPS is properly configured and you want all HTTP requests to
>  ## be redirected to HTTPS, uncomment the line below:
>  # request.requires_https()
> +import random
>  
>  ## app configuration made easy. Look inside private/appconfig.ini
>  from gluon.contrib.appconfig import AppConfig
> @@ -71,6 +72,8 @@ auth.settings.registration_requires_verification = False
>  auth.settings.registration_requires_approval = False
>  auth.settings.reset_password_requires_verification = True
>  
> +auth.settings.jwt_secret = myconf.take('auth.jwt_secret')
> +
>  #########################################################################
>  ## Define your tables below (or better in another model file) for example
>  ##
> diff --git a/applications/welcome/private/appconfig.ini
> b/applications/welcome/private/appconfig.ini
> index f45efbf..6db7544 100644
> --- a/applications/welcome/private/appconfig.ini
> +++ b/applications/welcome/private/appconfig.ini
> @@ -8,7 +8,7 @@ pool_size = 1
>  
>  ; smtp address and credentials
>  [smtp]
> -server = smtp.gmail.com:587
> +server = logging
>  sender = y...@gmail.com
>  login  = username:password
>  
> @@ -16,4 +16,7 @@ login  = username:password
>  ; form styling
>  [forms]
>  formstyle = bootstrap3_inline
> -separator =
> \ No newline at end of file
> +separator =
> +
> +[auth]
> +jwt_secret = very_secret_even_random
> diff --git a/gluon/tools.py b/gluon/tools.py
> index 9d90b9a..da18795 100644
> --- a/gluon/tools.py
> +++ b/gluon/tools.py
> @@ -1171,6 +1171,8 @@ class Auth(object):
>          remember_me_form=True,
>          allow_basic_login=False,
>          allow_basic_login_only=False,
> +        jwt_algorithm='HS512',
> +        jwt_secret='',
>          on_failed_authentication=lambda x: redirect(x),
>          formstyle=None,
>          label_separator=None,
> @@ -3724,6 +3726,16 @@ class Auth(object):
>              raise HTTP(403, 'ACCESS DENIED')
>          return self.messages.access_denied
>  
> +    def get_authorization_url(self, a, c, f, vars):
> +        keycode = self.get_authorization_code(a, c, f, vars)
> +        return URL(a=a, c=c, f=f, vars=dict(_keycode=keycode))
> +
> +    def get_authorization_code(self, a, c, f, vars):
> +        import jwt
> +        payload = {'_application':a, '_controller':c, '_function':f}
> +        payload.update(vars)
> +        return jwt.encode(payload, self.settings.jwt_secret,
> algorithm=self.settings.jwt_algorithm)
> +
>      def requires(self, condition, requires_login=True, otherwise=None):
>          """
>          Decorator that prevents access to action if not logged in
> @@ -3732,27 +3744,47 @@ class Auth(object):
>          def decorator(action):
>  
>              def f(*a, **b):
> +                r = current.request
>  
>                  basic_allowed, basic_accepted, user = self.basic()
>                  user = user or self.user
> -                if requires_login:
> +
> +                authorization_failed = False
> +                if self.settings.jwt_secret and r.vars._keycode:
> +                    import jwt
> +                    try:
> +                        encoded = r.vars._keycode
> +                        payload = jwt.decode(encoded,
> self.settings.jwt_secret, algorithms=[self.settings.jwt_algorithm])
> +                    except (jwt.DecodeError, jwt.InvalidTokenError):
> +                        authorization_failed = True
> +                    if not authorization_failed:
> +                        acf = tuple(payload.pop(s) for s in
> ('_application', '_controller', '_function'))
> +                        if acf != (r.application, r.controller,
> r.function):
> +                            authorization_failed = True
> +                        r.vars.clear()
> +                        r.vars.update(payload)
> +                        condition = True
> +                elif requires_login:
>                      if not user:
> -                        if current.request.ajax:
> -                            raise HTTP(401,
> self.messages.ajax_failed_authentication)
> -                        elif not otherwise is None:
> -                            if callable(otherwise):
> -                                return otherwise()
> -                            redirect(otherwise)
> -                        elif self.settings.allow_basic_login_only or \
> -                                basic_accepted or
> current.request.is_restful:
> -                            raise HTTP(403, "Not authorized")
> -                        else:
> -                            next = self.here()
> -                            current.session.flash =
> current.response.flash
> -                            return call_or_redirect(
> -                                self.settings.on_failed_authentication,
> -                                self.settings.login_url +
> -                                    '?_next=' + urllib.quote(next))
> +                        authorization_failed = True
> +
> +                if authorization_failed:
> +                    if r.ajax:
> +                        raise HTTP(401,
> self.messages.ajax_failed_authentication)
> +                    elif not otherwise is None:
> +                        if callable(otherwise):
> +                            return otherwise()
> +                        redirect(otherwise)
> +                    elif self.settings.allow_basic_login_only or \
> +                            basic_accepted or r.is_restful:
> +                        raise HTTP(403, "Not authorized")
> +                    else:
> +                        next = self.here()
> +                        current.session.flash = current.response.flash
> +                        return call_or_redirect(
> +                            self.settings.on_failed_authentication,
> +                            self.settings.login_url +
> +                                '?_next=' + urllib.quote(next))
>  
>                  if callable(condition):
>                      flag = condition()

-- 
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
--- 
You received this message because you are subscribed to the Google Groups 
"web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to web2py+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
diff --git a/applications/welcome/controllers/default.py b/applications/welcome/controllers/default.py
index c775603..e99ef0f 100644
--- a/applications/welcome/controllers/default.py
+++ b/applications/welcome/controllers/default.py
@@ -57,4 +57,10 @@ def call():
     """
     return service()
 
+@auth.requires_membership('masters')
+def master_zone():
+    return dict(a='Hello, Master', b=request.vars.message)
 
+def get_master_code():
+    url = auth.get_authorization_url(a='welcome', c='default', f='master_zone', vars=dict(message='Greetings'))
+    return dict(a = A('copy this link', _href=url))
diff --git a/applications/welcome/languages/es.py b/applications/welcome/languages/es.py
index 7579cc3..54a57c6 100644
--- a/applications/welcome/languages/es.py
+++ b/applications/welcome/languages/es.py
@@ -55,6 +55,7 @@
 'Available Databases and Tables': 'Bases de datos y tablas disponibles',
 'Back': 'Atrás',
 'Buy this book': 'Compra este libro',
+"Buy web2py's book": "Buy web2py's book",
 'Cache': 'Caché',
 'cache': 'caché',
 'Cache Keys': 'Llaves de la Caché',
@@ -83,6 +84,7 @@
 'compile': 'compilar',
 'compiled application removed': 'aplicación compilada eliminada',
 'Components and Plugins': 'Componentes y Plugins',
+'Config.ini': 'Config.ini',
 'contains': 'contiene',
 'Controller': 'Controlador',
 'Controllers': 'Controladores',
@@ -119,6 +121,7 @@
 'Description': 'Descripción',
 'design': 'diseño',
 'DESIGN': 'DISEÑO',
+'Design': 'Design',
 'Design for': 'Diseño por',
 'detecting': 'detectando',
 'DISK': 'DISCO',
@@ -145,6 +148,7 @@
 'End of impersonation': 'Fin de suplantación',
 'enter a number between %(min)g and %(max)g': 'introduzca un número entre %(min)g y %(max)g',
 'enter a value': 'introduzca un valor',
+'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g',
 'enter an integer between %(min)g and %(max)g': 'introduzca un entero entre %(min)g y %(max)g',
 'enter date and time as %(format)s': 'introduzca fecha y hora como %(format)s',
 'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"',
@@ -177,6 +181,7 @@
 'Groups': 'Grupos',
 'Hello World': 'Hola Mundo',
 'help': 'ayuda',
+'Helping web2py': 'Helping web2py',
 'Home': 'Inicio',
 'How did you get here?': '¿Cómo llegaste aquí?',
 'htmledit': 'htmledit',
@@ -217,6 +222,7 @@
 'License for': 'Licencia para',
 'Live Chat': 'Chat en vivo',
 'loading...': 'cargando...',
+'Log In': 'Log In',
 'Logged in': 'Sesión iniciada',
 'Logged out': 'Sesión finalizada',
 'Login': 'Inicio de sesión',
@@ -256,6 +262,7 @@
 'not in': 'no en',
 'Object or table name': 'Nombre del objeto o tabla',
 'Old password': 'Contraseña vieja',
+'Online book': 'Online book',
 'Online examples': 'Ejemplos en línea',
 'Or': 'O',
 'or import from csv file': 'o importar desde archivo CSV',
@@ -319,6 +326,7 @@
 'Services': 'Servicios',
 'session expired': 'sesión expirada',
 'shell': 'terminal',
+'Sign Up': 'Sign Up',
 'site': 'sitio',
 'Size of cache:': 'Tamaño de la Caché:',
 'some files could not be removed': 'algunos archivos no pudieron ser removidos',
diff --git a/applications/welcome/models/db.py b/applications/welcome/models/db.py
index 606dc6f..3438621 100644
--- a/applications/welcome/models/db.py
+++ b/applications/welcome/models/db.py
@@ -8,6 +8,7 @@
 ## if SSL/HTTPS is properly configured and you want all HTTP requests to
 ## be redirected to HTTPS, uncomment the line below:
 # request.requires_https()
+import random
 
 ## app configuration made easy. Look inside private/appconfig.ini
 from gluon.contrib.appconfig import AppConfig
@@ -71,6 +72,8 @@ auth.settings.registration_requires_verification = False
 auth.settings.registration_requires_approval = False
 auth.settings.reset_password_requires_verification = True
 
+auth.settings.jwt_secret = myconf.take('auth.jwt_secret')
+
 #########################################################################
 ## Define your tables below (or better in another model file) for example
 ##
diff --git a/applications/welcome/private/appconfig.ini b/applications/welcome/private/appconfig.ini
index f45efbf..6db7544 100644
--- a/applications/welcome/private/appconfig.ini
+++ b/applications/welcome/private/appconfig.ini
@@ -8,7 +8,7 @@ pool_size = 1
 
 ; smtp address and credentials
 [smtp]
-server = smtp.gmail.com:587
+server = logging
 sender = y...@gmail.com
 login  = username:password
 
@@ -16,4 +16,7 @@ login  = username:password
 ; form styling
 [forms]
 formstyle = bootstrap3_inline
-separator = 
\ No newline at end of file
+separator = 
+
+[auth]
+jwt_secret = very_secret_even_random
diff --git a/gluon/tools.py b/gluon/tools.py
index 9d90b9a..da18795 100644
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -1171,6 +1171,8 @@ class Auth(object):
         remember_me_form=True,
         allow_basic_login=False,
         allow_basic_login_only=False,
+        jwt_algorithm='HS512',
+        jwt_secret='',
         on_failed_authentication=lambda x: redirect(x),
         formstyle=None,
         label_separator=None,
@@ -3724,6 +3726,16 @@ class Auth(object):
             raise HTTP(403, 'ACCESS DENIED')
         return self.messages.access_denied
 
+    def get_authorization_url(self, a, c, f, vars):
+        keycode = self.get_authorization_code(a, c, f, vars)
+        return URL(a=a, c=c, f=f, vars=dict(_keycode=keycode))
+
+    def get_authorization_code(self, a, c, f, vars):
+        import jwt
+        payload = {'_application':a, '_controller':c, '_function':f}
+        payload.update(vars)
+        return jwt.encode(payload, self.settings.jwt_secret, algorithm=self.settings.jwt_algorithm)
+
     def requires(self, condition, requires_login=True, otherwise=None):
         """
         Decorator that prevents access to action if not logged in
@@ -3732,27 +3744,47 @@ class Auth(object):
         def decorator(action):
 
             def f(*a, **b):
+                r = current.request
 
                 basic_allowed, basic_accepted, user = self.basic()
                 user = user or self.user
-                if requires_login:
+
+                authorization_failed = False
+                if self.settings.jwt_secret and r.vars._keycode:
+                    import jwt
+                    try:
+                        encoded = r.vars._keycode
+                        payload = jwt.decode(encoded, self.settings.jwt_secret, algorithms=[self.settings.jwt_algorithm])
+                    except (jwt.DecodeError, jwt.InvalidTokenError):
+                        authorization_failed = True
+                    if not authorization_failed:
+                        acf = tuple(payload.pop(s) for s in ('_application', '_controller', '_function'))
+                        if acf != (r.application, r.controller, r.function):
+                            authorization_failed = True
+                        r.vars.clear()
+                        r.vars.update(payload)
+                        condition = True
+                elif requires_login:
                     if not user:
-                        if current.request.ajax:
-                            raise HTTP(401, self.messages.ajax_failed_authentication)
-                        elif not otherwise is None:
-                            if callable(otherwise):
-                                return otherwise()
-                            redirect(otherwise)
-                        elif self.settings.allow_basic_login_only or \
-                                basic_accepted or current.request.is_restful:
-                            raise HTTP(403, "Not authorized")
-                        else:
-                            next = self.here()
-                            current.session.flash = current.response.flash
-                            return call_or_redirect(
-                                self.settings.on_failed_authentication,
-                                self.settings.login_url +
-                                    '?_next=' + urllib.quote(next))
+                        authorization_failed = True
+
+                if authorization_failed:
+                    if r.ajax:
+                        raise HTTP(401, self.messages.ajax_failed_authentication)
+                    elif not otherwise is None:
+                        if callable(otherwise):
+                            return otherwise()
+                        redirect(otherwise)
+                    elif self.settings.allow_basic_login_only or \
+                            basic_accepted or r.is_restful:
+                        raise HTTP(403, "Not authorized")
+                    else:
+                        next = self.here()
+                        current.session.flash = current.response.flash
+                        return call_or_redirect(
+                            self.settings.on_failed_authentication,
+                            self.settings.login_url +
+                                '?_next=' + urllib.quote(next))
 
                 if callable(condition):
                     flag = condition()

Reply via email to