| Index: appengine/components/components/auth/ui/ui.py
|
| diff --git a/appengine/components/components/auth/ui/ui.py b/appengine/components/components/auth/ui/ui.py
|
| index 10f9a6c533127f1202fae6e207a1af2ff0bc15e3..c0b3956a2df13c86bb0832fdfb2dd4af58c96d9c 100644
|
| --- a/appengine/components/components/auth/ui/ui.py
|
| +++ b/appengine/components/components/auth/ui/ui.py
|
| @@ -115,81 +115,38 @@ def redirect_ui_on_replica(method):
|
| return wrapper
|
|
|
|
|
| -class UIHandler(handler.AuthenticatingHandler):
|
| - """Renders Jinja templates extending base.html or base_minimal.html."""
|
| +################################################################################
|
| +## Admin routes. The use cookies and GAE's "is_current_user_admin" for authn.
|
| +
|
| +
|
| +class AdminPageHandler(handler.AuthenticatingHandler):
|
| + """Base class for handlers involved in bootstrap processes."""
|
|
|
| # TODO(vadimsh): Enable CSP nonce for styles too. We'll need to get rid of
|
| # all 'style=...' attributes first.
|
| csp_use_script_nonce = True
|
|
|
| + @classmethod
|
| + def get_auth_methods(cls, conf):
|
| + # This method sets 'is_superuser' bit for GAE-level admins.
|
| + return [handler.gae_cookie_authentication]
|
| +
|
| def reply(self, path, env=None, status=200):
|
| """Render template |path| to response using given environment.
|
|
|
| - Optional keys from |env| that base.html uses:
|
| - css_file: URL to a file with page specific styles, relative to site root.
|
| - js_file: URL to a file with page specific Javascript code, relative to
|
| - site root. File should define global object named same as a filename,
|
| - i.e. '/auth/static/js/api.js' should define global object 'api' that
|
| - incapsulates functionality implemented in the module.
|
| - navbar_tab_id: id a navbar tab to highlight.
|
| - page_title: title of an HTML page.
|
| -
|
| Args:
|
| path: path to a template, relative to templates/.
|
| env: additional environment dict to use when rendering the template.
|
| status: HTTP status code to return.
|
| """
|
| - env = (env or {}).copy()
|
| - env.setdefault('css_file', None)
|
| - env.setdefault('js_file', None)
|
| - env.setdefault('navbar_tab_id', None)
|
| - env.setdefault('page_title', 'Untitled')
|
| -
|
| - # This goes to both Jinja2 env and Javascript config object.
|
| - user = self.get_current_user()
|
| - common = {
|
| - 'account_picture': user.picture() if user else None,
|
| - 'auth_service_config_locked': False, # overridden in auth_service
|
| - 'is_admin': api.is_admin(),
|
| - 'login_url': self.create_login_url(self.request.url),
|
| - 'logout_url': self.create_logout_url('/'),
|
| - 'using_gae_auth': self.auth_method == handler.gae_cookie_authentication,
|
| - 'xsrf_token': self.generate_xsrf_token(),
|
| - }
|
| - if _ui_env_callback:
|
| - common.update(_ui_env_callback(self))
|
| -
|
| - # Name of Javascript module with page code.
|
| - js_module_name = None
|
| - if env['js_file']:
|
| - assert env['js_file'].endswith('.js')
|
| - js_module_name = os.path.basename(env['js_file'])[:-3]
|
| -
|
| - # This will be accessible from Javascript as global 'config' variable.
|
| - js_config = {
|
| - 'identity': api.get_current_identity().to_bytes(),
|
| - }
|
| - js_config.update(common)
|
| -
|
| - # Jinja2 environment to use to render a template.
|
| full_env = {
|
| 'app_name': _ui_app_name,
|
| - 'app_revision_url': utils.get_app_revision_url(),
|
| - 'app_version': utils.get_app_version(),
|
| - 'config': json.dumps(js_config),
|
| 'csp_nonce': self.csp_nonce,
|
| 'identity': api.get_current_identity(),
|
| - 'js_module_name': js_module_name,
|
| - 'navbar': [
|
| - (cls.navbar_tab_id, cls.navbar_tab_title, cls.navbar_tab_url)
|
| - for cls in _ui_navbar_tabs
|
| - if cls.is_visible()
|
| - ],
|
| + 'logout_url': json.dumps(self.create_logout_url('/')), # see base.html
|
| + 'xsrf_token': self.generate_xsrf_token(),
|
| }
|
| - full_env.update(common)
|
| - full_env.update(env)
|
| -
|
| - # Render it.
|
| + full_env.update(env or {})
|
| self.response.set_status(status)
|
| self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
| self.response.write(template.render(path, full_env))
|
| @@ -200,7 +157,7 @@ class UIHandler(handler.AuthenticatingHandler):
|
| 'page_title': 'Access Denied',
|
| 'error': error,
|
| }
|
| - self.reply('auth/access_denied.html', env=env, status=401)
|
| + self.reply('auth/admin/access_denied.html', env=env, status=401)
|
|
|
| def authorization_error(self, error):
|
| """Redirects to login or shows 'Access Denied' page."""
|
| @@ -221,19 +178,10 @@ class UIHandler(handler.AuthenticatingHandler):
|
| 'page_title': 'Access Denied',
|
| 'error': error,
|
| }
|
| - self.reply('auth/access_denied.html', env=env, status=403)
|
| + self.reply('auth/admin/access_denied.html', env=env, status=403)
|
|
|
|
|
| -class MainHandler(UIHandler):
|
| - """Redirects to first navbar tab."""
|
| - @redirect_ui_on_replica
|
| - @api.require(acl.has_access)
|
| - def get(self):
|
| - assert _ui_navbar_tabs
|
| - self.redirect(_ui_navbar_tabs[0].navbar_tab_url)
|
| -
|
| -
|
| -class BootstrapHandler(UIHandler):
|
| +class BootstrapHandler(AdminPageHandler):
|
| """Creates Administrators group (if necessary) and adds current caller to it.
|
|
|
| Requires Appengine level Admin access for its handlers, since Administrators
|
| @@ -242,11 +190,6 @@ class BootstrapHandler(UIHandler):
|
| Used during bootstrap of a new service instance.
|
| """
|
|
|
| - @classmethod
|
| - def get_auth_methods(cls, conf):
|
| - # This method sets 'is_superuser' bit for GAE-level admins.
|
| - return [handler.gae_cookie_authentication]
|
| -
|
| @forbid_ui_on_replica
|
| @api.require(api.is_superuser)
|
| def get(self):
|
| @@ -255,7 +198,7 @@ class BootstrapHandler(UIHandler):
|
| 'admin_group': model.ADMIN_GROUP,
|
| 'return_url': self.request.get('r') or '',
|
| }
|
| - self.reply('auth/bootstrap.html', env)
|
| + self.reply('auth/admin/bootstrap.html', env)
|
|
|
| @forbid_ui_on_replica
|
| @api.require(api.is_superuser)
|
| @@ -269,10 +212,10 @@ class BootstrapHandler(UIHandler):
|
| 'added': added,
|
| 'return_url': self.request.get('return_url') or '',
|
| }
|
| - self.reply('auth/bootstrap_done.html', env)
|
| + self.reply('auth/admin/bootstrap_done.html', env)
|
|
|
|
|
| -class BootstrapOAuthHandler(UIHandler):
|
| +class BootstrapOAuthHandler(AdminPageHandler):
|
| """Page to set OAuth2 client ID used by the main web UI.
|
|
|
| Requires Appengine level Admin access for its handlers, since without client
|
| @@ -282,11 +225,6 @@ class BootstrapOAuthHandler(UIHandler):
|
| also available after the service is linked to some primary Auth service.
|
| """
|
|
|
| - @classmethod
|
| - def get_auth_methods(cls, conf):
|
| - # This method sets 'is_superuser' bit for GAE-level admins.
|
| - return [handler.gae_cookie_authentication]
|
| -
|
| @api.require(api.is_superuser)
|
| def get(self):
|
| self.show_page(web_client_id=api.get_web_client_id_uncached())
|
| @@ -303,10 +241,10 @@ class BootstrapOAuthHandler(UIHandler):
|
| 'web_client_id': web_client_id or '',
|
| 'saved': saved,
|
| }
|
| - self.reply('auth/bootstrap_oauth.html', env)
|
| + self.reply('auth/admin/bootstrap_oauth.html', env)
|
|
|
|
|
| -class LinkToPrimaryHandler(UIHandler):
|
| +class LinkToPrimaryHandler(AdminPageHandler):
|
| """A page with confirmation of Primary <-> Replica linking request.
|
|
|
| URL to that page is generated by a Primary service.
|
| @@ -321,11 +259,6 @@ class LinkToPrimaryHandler(UIHandler):
|
| self.abort(400)
|
| return
|
|
|
| - @classmethod
|
| - def get_auth_methods(cls, conf):
|
| - # This method sets 'is_superuser' bit for GAE-level admins.
|
| - return [handler.gae_cookie_authentication]
|
| -
|
| @forbid_ui_on_replica
|
| @api.require(api.is_superuser)
|
| def get(self):
|
| @@ -336,7 +269,7 @@ class LinkToPrimaryHandler(UIHandler):
|
| 'primary_id': ticket.primary_id,
|
| 'primary_url': ticket.primary_url,
|
| }
|
| - self.reply('auth/linking.html', env)
|
| + self.reply('auth/admin/linking.html', env)
|
|
|
| @forbid_ui_on_replica
|
| @api.require(api.is_superuser)
|
| @@ -356,7 +289,133 @@ class LinkToPrimaryHandler(UIHandler):
|
| 'primary_url': ticket.primary_url,
|
| 'success': success,
|
| }
|
| - self.reply('auth/linking_done.html', env)
|
| + self.reply('auth/admin/linking_done.html', env)
|
| +
|
| +
|
| +################################################################################
|
| +## Web UI routes.
|
| +
|
| +# TODO(vadimsh): Switch them to use OAuth for authentication.
|
| +
|
| +
|
| +class UIHandler(handler.AuthenticatingHandler):
|
| + """Renders Jinja templates extending base.html."""
|
| +
|
| + # TODO(vadimsh): Enable CSP nonce for styles too. We'll need to get rid of
|
| + # all 'style=...' attributes first.
|
| + csp_use_script_nonce = True
|
| +
|
| + def reply(self, path, env=None, status=200):
|
| + """Renders template |path| to the HTTP response using given environment.
|
| +
|
| + Optional keys from |env| that base.html uses:
|
| + css_file: URL to a file with page specific styles, relative to site root.
|
| + js_file: URL to a file with page specific Javascript code, relative to
|
| + site root. File should define global object named same as a filename,
|
| + i.e. '/auth/static/js/api.js' should define global object 'api' that
|
| + incapsulates functionality implemented in the module.
|
| + navbar_tab_id: id of a navbar tab to highlight.
|
| + page_title: title of an HTML page.
|
| +
|
| + Args:
|
| + path: path to a template, relative to templates/.
|
| + env: additional environment dict to use when rendering the template.
|
| + status: HTTP status code to return.
|
| + """
|
| + env = (env or {}).copy()
|
| + env.setdefault('css_file', None)
|
| + env.setdefault('js_file', None)
|
| + env.setdefault('navbar_tab_id', None)
|
| + env.setdefault('page_title', 'Untitled')
|
| +
|
| + # This goes to both Jinja2 env and Javascript config object.
|
| + user = self.get_current_user()
|
| + common = {
|
| + 'account_picture': user.picture() if user else None,
|
| + 'auth_service_config_locked': False, # overridden in auth_service
|
| + 'is_admin': api.is_admin(),
|
| + 'login_url': self.create_login_url(self.request.url),
|
| + 'logout_url': self.create_logout_url('/'),
|
| + 'using_gae_auth': self.auth_method == handler.gae_cookie_authentication,
|
| + 'xsrf_token': self.generate_xsrf_token(),
|
| + }
|
| + if _ui_env_callback:
|
| + common.update(_ui_env_callback(self))
|
| +
|
| + # Name of Javascript module with page code.
|
| + js_module_name = None
|
| + if env['js_file']:
|
| + assert env['js_file'].endswith('.js')
|
| + js_module_name = os.path.basename(env['js_file'])[:-3]
|
| +
|
| + # This will be accessible from Javascript as global 'config' variable.
|
| + js_config = {
|
| + 'identity': api.get_current_identity().to_bytes(),
|
| + }
|
| + js_config.update(common)
|
| +
|
| + # Jinja2 environment to use to render a template.
|
| + full_env = {
|
| + 'app_name': _ui_app_name,
|
| + 'app_revision_url': utils.get_app_revision_url(),
|
| + 'app_version': utils.get_app_version(),
|
| + 'config': json.dumps(js_config),
|
| + 'csp_nonce': self.csp_nonce,
|
| + 'identity': api.get_current_identity(),
|
| + 'js_module_name': js_module_name,
|
| + 'navbar': [
|
| + (cls.navbar_tab_id, cls.navbar_tab_title, cls.navbar_tab_url)
|
| + for cls in _ui_navbar_tabs
|
| + if cls.is_visible()
|
| + ],
|
| + }
|
| + full_env.update(common)
|
| + full_env.update(env)
|
| +
|
| + # Render it.
|
| + self.response.set_status(status)
|
| + self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
| + self.response.write(template.render(path, full_env))
|
| +
|
| + def authentication_error(self, error):
|
| + """Shows 'Access denied' page."""
|
| + # TODO(vadimsh): This will be deleted once we use Google Sign-In.
|
| + env = {
|
| + 'page_title': 'Access Denied',
|
| + 'error': error,
|
| + }
|
| + self.reply('auth/access_denied.html', env=env, status=401)
|
| +
|
| + def authorization_error(self, error):
|
| + """Redirects to login or shows 'Access Denied' page."""
|
| + # TODO(vadimsh): This will be deleted once we use Google Sign-In.
|
| + # Not authenticated or used IP whitelist for auth -> redirect to login.
|
| + # Bots doesn't use UI, and users should always use real accounts.
|
| + ident = api.get_current_identity()
|
| + if ident.is_anonymous or ident.is_bot:
|
| + self.redirect(self.create_login_url(self.request.url))
|
| + return
|
| +
|
| + # Admin group is empty -> redirect to bootstrap procedure to create it.
|
| + if model.is_empty_group(model.ADMIN_GROUP):
|
| + self.redirect_to('bootstrap')
|
| + return
|
| +
|
| + # No access.
|
| + env = {
|
| + 'page_title': 'Access Denied',
|
| + 'error': error,
|
| + }
|
| + self.reply('auth/access_denied.html', env=env, status=403)
|
| +
|
| +
|
| +class MainHandler(UIHandler):
|
| + """Redirects to first navbar tab."""
|
| + @redirect_ui_on_replica
|
| + @api.require(acl.has_access)
|
| + def get(self):
|
| + assert _ui_navbar_tabs
|
| + self.redirect(_ui_navbar_tabs[0].navbar_tab_url)
|
|
|
|
|
| class UINavbarTabHandler(UIHandler):
|
|
|