* Added Google Platform Library

* Added Meta Element for Client ID

* Added Google Sign-In Button

* Authenticating the id_token on our backend

* Saving id_token in flask session, using the id_token to fetch the current user and replaced the usages of AppEngine Users API (not from *_tests.py)

* Correct the flow on pressing SignIn and SignOut

* Code refactor

* Added Comment for is_current_user_admin

* Supporting GAE Users library for post request

* Made some fixes

* Changed Admin User condition

* Reloading only on 200 response code

* Do not require sign in and xsrf token while sending post request for login

* Sign Out using Google Sign In if cookie is not set after login

* Clearing the session if the id_token stored in the session variable becomes invalid or expires

* Replaced GAE Users from tests

* Replaced GAE users with framework users in tests.py
This commit is contained in:
Shivam Agarwal 2021-05-07 05:07:30 +05:30 коммит произвёл GitHub
Родитель 12ae9e0166
Коммит 8fbebb7989
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
24 изменённых файлов: 433 добавлений и 42 удалений

51
api/login_api.py Normal file
Просмотреть файл

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright 2021 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import division
from __future__ import print_function
import logging
from google.oauth2 import id_token
from google.auth.transport import requests
from flask import session
from framework import basehandlers
# from framework import permissions
# from framework import ramcache
# from internals import models
class LoginAPI(basehandlers.APIHandler):
"""Create a session using the id_token generated by Google Sign-In."""
def do_post(self):
token = self.get_param('id_token')
message = "Unable to Authenticate"
try:
CLIENT_ID = '77756740465-e5r4o15qg4hkdfiucjpl231o79k3ipjv.apps.googleusercontent.com'
idinfo = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
# userid = idinfo['sub']
# email = idinfo['email']
session["id_token"] = token
message = "Done"
# print(idinfo['email'], file=sys.stderr)
except ValueError:
message = "Invalid token"
pass
return {'message': message}

37
api/logout_api.py Normal file
Просмотреть файл

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2021 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import division
from __future__ import print_function
import logging
from google.oauth2 import id_token
from google.auth.transport import requests
from flask import session
from framework import basehandlers
# from framework import permissions
# from framework import ramcache
# from internals import models
class LogoutAPI(basehandlers.APIHandler):
"""Create a session using the id_token generated by Google Sign-In."""
def do_post(self):
session.clear()
return {'message': 'Done'}

Просмотреть файл

@ -22,7 +22,8 @@ import datetime
import json
import logging
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from framework import basehandlers
from internals import models

Просмотреть файл

@ -22,7 +22,8 @@ import datetime
import mock
import flask
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from api import metricsdata
from internals import models

Просмотреть файл

@ -24,6 +24,9 @@ from api import stars_api
from api import token_refresh_api
from framework import basehandlers
from api import login_api
from api import logout_api
# TODO(jrobbins): Advance this to v1 once we have it fleshed out
API_BASE = '/api/v0'
@ -34,7 +37,8 @@ app = basehandlers.FlaskApplication([
('/features/<int:feature_id>/approvals/<int:field_id>',
approvals_api.ApprovalsAPI),
# ('/features/<int:feature_id>/approvals/<int:field_id>/comments', TODO),
('/login', login_api.LoginAPI),
('/logout', logout_api.LogoutAPI),
('/currentuser/stars', stars_api.StarsAPI),
('/currentuser/cues', cues_api.CuesAPI),
('/currentuser/token', token_refresh_api.TokenRefreshAPI),

Просмотреть файл

@ -4,8 +4,8 @@ from __future__ import print_function
import datetime
from google.appengine.ext import db
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
def email_to_list():
def wrapper(value):

Просмотреть файл

@ -25,7 +25,7 @@ import flask
import flask.views
import werkzeug.exceptions
from google.appengine.api import users
from google.appengine.api import users as gae_users
from google.appengine.ext import db
import settings
@ -39,6 +39,12 @@ from internals import models
from django.template.loader import render_to_string
import django
from google.oauth2 import id_token
from google.auth.transport import requests
from flask import session
import sys
from framework import users
# Initialize django so that it'll function when run as a standalone script.
# https://django.readthedocs.io/en/latest/releases/1.7.html#standalone-scripts
django.setup()
@ -71,10 +77,16 @@ class BaseHandler(flask.views.MethodView):
def get_current_user(self, required=False):
# TODO(jrobbins): oauth support
user = users.get_current_user()
if required and not user:
current_user = None
if self.request.method == 'POST':
current_user = users.get_current_user() or gae_users.get_current_user()
else:
current_user = users.get_current_user()
if required and not current_user:
self.abort(403, msg='User must be signed in')
return user
return current_user
def get_param(
self, name, default=None, required=True, validator=None, allowed=None):
@ -149,7 +161,10 @@ class APIHandler(BaseHandler):
def post(self, *args, **kwargs):
"""Handle an incoming HTTP POST request."""
self.require_signed_in_and_xsrf_token()
is_login_request = str(self.request.url_rule) == '/api/v0/login'
if not is_login_request:
self.require_signed_in_and_xsrf_token()
headers = self.get_headers()
ramcache.check_for_distributed_invalidation()
handler_data = self.do_post(*args, **kwargs)
@ -286,7 +301,7 @@ class FlaskHandler(BaseHandler):
if user:
user_pref = models.UserPref.get_signed_in_user_pref()
common_data['login'] = (
'Sign out', users.create_logout_url(dest_url=current_path))
'Sign out', "SignOut")
common_data['user'] = {
'can_create_feature': permissions.can_create_feature(user),
'can_edit': permissions.can_edit_any_feature(user),
@ -299,7 +314,7 @@ class FlaskHandler(BaseHandler):
else:
common_data['user'] = None
common_data['login'] = (
'Sign in', users.create_login_url(dest_url=current_path))
'Sign in', "Sign In")
common_data['xsrf_token'] = xsrf.generate_token(None)
common_data['xsrf_token_expires'] = 0
return common_data

Просмотреть файл

@ -24,7 +24,8 @@ import flask
import flask.views
import werkzeug.exceptions # Flask HTTP stuff.
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from framework import basehandlers
from framework import xsrf

Просмотреть файл

@ -19,16 +19,15 @@ from __future__ import print_function
import logging
import flask
from google.appengine.api import users
from google.appengine.api import users as gae_users
from framework import users
from internals import models
def can_admin_site(user):
"""Return True if the current user is allowed to administer the site."""
# A user is an admin if they are an admin of the GAE project.
# TODO(jrobbins): delete this statement after legacy admins moved to AppUser.
if users.is_current_user_admin():
if gae_users.is_current_user_admin():
return True
# A user is an admin if they have an AppUser entity that has is_admin set.
@ -99,7 +98,7 @@ def _reject_or_proceed(
# Give the user a chance to sign in
if not user and req.method == 'GET':
return handler_obj.redirect(users.create_login_url(req.full_path))
return handler_obj.redirect('/features?loginStatus=False')
if not perm_function(user):
handler_obj.abort(403)

Просмотреть файл

@ -21,7 +21,8 @@ import testing_config # Must be imported before the module under test.
import mock
import werkzeug.exceptions # Flask HTTP stuff.
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from framework import basehandlers
from framework import permissions

199
framework/users.py Normal file
Просмотреть файл

@ -0,0 +1,199 @@
import sys
from flask import session
from google.oauth2 import id_token
from google.auth.transport import requests
class User(object):
"""Provides the email address, nickname, and ID for a user.
A nickname is a human-readable string that uniquely identifies a Google user,
akin to a username. For some users, this nickname is an email address, but for
other users, a different nickname is used.
A user is a Google Accounts user.
`federated_identity` and `federated_provider` are decommissioned and should
not be used.
This class is based on google.appengine.api.users.User class
"""
__user_id = None
__federated_identity = None
__federated_provider = None
def __init__(self, email=None, _auth_domain=None,
_user_id=None, federated_identity=None, federated_provider=None,
_strict_mode=True):
"""Constructor.
Args:
email: An optional string of the user's email address. It defaults to
the current user's email address.
federated_identity: Decommissioned, don't use.
federated_provider: Decommissioned, don't use.
Raises:
UserNotFoundError: If the user is not logged in and both `email` and
`federated_identity` are empty.
"""
self.__email = email
self.__federated_identity = federated_identity
self.__federated_provider = federated_provider
self.__auth_domain = _auth_domain
self.__user_id = _user_id or None
def nickname(self):
"""Returns the user's nickname.
The nickname will be a unique, human readable identifier for this user with
respect to this application. It will be an email address for some users,
and part of the email address for some users.
Returns:
The nickname of the user as a string.
"""
if (self.__email and self.__auth_domain and
self.__email.endswith('@' + self.__auth_domain)):
suffix_len = len(self.__auth_domain) + 1
return self.__email[:-suffix_len]
elif self.__federated_identity:
return self.__federated_identity
else:
return self.__email
def email(self):
"""Returns the user's email address."""
return self.__email
def user_id(self):
"""Obtains the user ID of the user.
Returns:
A permanent unique identifying string or `None`. If the email address was
set explicity, this will return `None`.
"""
return self.__user_id
def auth_domain(self):
"""Obtains the user's authentication domain.
Returns:
A string containing the authentication domain. This method is internal and
should not be used by client applications.
"""
return self.__auth_domain
def federated_identity(self):
"""Decommissioned, don't use.
Returns:
A string containing the federated identity of the user. If the user is not
a federated user, `None` is returned.
"""
return self.__federated_identity
def federated_provider(self):
"""Decommissioned, don't use.
Returns:
A string containing the federated provider. If the user is not a federated
user, `None` is returned.
"""
return self.__federated_provider
def __unicode__(self):
return six_subset.text_type(self.nickname())
def __str__(self):
return str(self.nickname())
def __repr__(self):
values = []
if self.__email:
values.append("email='%s'" % self.__email)
if self.__federated_identity:
values.append("federated_identity='%s'" % self.__federated_identity)
if self.__user_id:
values.append("_user_id='%s'" % self.__user_id)
return 'users.User(%s)' % ','.join(values)
def __hash__(self):
if self.__federated_identity:
return hash((self.__federated_identity, self.__auth_domain))
else:
return hash((self.__email, self.__auth_domain))
def __cmp__(self, other):
if not isinstance(other, User):
return NotImplemented
if self.__federated_identity:
return cmp((self.__federated_identity, self.__auth_domain),
(other.__federated_identity, other.__auth_domain))
else:
return cmp((self.__email, self.__auth_domain),
(other.__email, other.__auth_domain))
def get_current_user():
"""Retrieves information associated with the user that is making a request.
Returns:
"""
try:
return User()
except UserNotFoundError:
return None
def is_current_user_admin():
"""Specifies whether the user making a request is an application admin.
Because administrator status is not persisted in the datastore,
`is_current_user_admin()` is a separate function rather than a member function
of the `User` class. The status only exists for the user making the current
request.
Returns:
`True` if the user is an administrator; all other user types return `False`.
"""
# This environment variable was set by GAE based on a GAE session cookie.
# With Google Sign-In, it will probably never be present. Hence, currently is always False
# TODO (jrobbins): Implement this method
return (os.environ.get('USER_IS_ADMIN', '0')) == '1'
def get_current_user():
token = session.get('id_token')
current_user = None
if token:
try:
CLIENT_ID = '77756740465-e5r4o15qg4hkdfiucjpl231o79k3ipjv.apps.googleusercontent.com'
idinfo = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
current_user = User(email=idinfo['email'], _user_id=idinfo['sub'])
except ValueError:
# Remove the id_token from session if it is invalid or expired
session.clear()
current_user = None
pass
return current_user
def is_current_user_admin():
return False

Просмотреть файл

@ -23,7 +23,8 @@ import logging
import time
import traceback
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
import settings

Просмотреть файл

@ -28,7 +28,8 @@ from google.appengine.ext import db
from google.appengine.api import mail
from framework import ramcache
import requests
from google.appengine.api import users
from google.appengine.api import users as gae_users
from framework import users
from framework import cloud_tasks_helpers
import settings
@ -346,7 +347,7 @@ class DictModel(db.Model):
output[key] = {'lat': value.lat, 'lon': value.lon}
elif isinstance(value, db.Model):
output[key] = to_dict(value)
elif isinstance(value, users.User):
elif isinstance(value, gae_users.User):
output[key] = value.email()
else:
raise ValueError('cannot encode ' + repr(prop))

Просмотреть файл

@ -20,7 +20,8 @@ import testing_config # Must be imported before the module under test.
import mock
from framework import ramcache
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from internals import models
@ -154,13 +155,15 @@ class UserPrefTest(unittest.TestCase):
self.user_pref_1.delete()
self.user_pref_2.delete()
@mock.patch('google.appengine.api.users.get_current_user')
# @mock.patch('google.appengine.api.users.get_current_user')
@mock.patch('framework.users.get_current_user')
def test_get_signed_in_user_pref__anon(self, mock_get_current_user):
mock_get_current_user.return_value = None
actual = models.UserPref.get_signed_in_user_pref()
self.assertIsNone(actual)
@mock.patch('google.appengine.api.users.get_current_user')
# @mock.patch('google.appengine.api.users.get_current_user')
@mock.patch('framework.users.get_current_user')
def test_get_signed_in_user_pref__first_time(self, mock_get_current_user):
mock_get_current_user.return_value = testing_config.Blank(
email=lambda: 'user1@example.com')
@ -171,7 +174,8 @@ class UserPrefTest(unittest.TestCase):
self.assertEqual(True, actual.notify_as_starrer)
self.assertEqual(False, actual.bounced)
@mock.patch('google.appengine.api.users.get_current_user')
# @mock.patch('google.appengine.api.users.get_current_user')
@mock.patch('framework.users.get_current_user')
def test_get_signed_in_user_pref__had_pref(self, mock_get_current_user):
mock_get_current_user.return_value = testing_config.Blank(
email=lambda: 'user2@example.com')
@ -185,7 +189,8 @@ class UserPrefTest(unittest.TestCase):
self.assertEqual(False, actual.notify_as_starrer)
self.assertEqual(True, actual.bounced)
@mock.patch('google.appengine.api.users.get_current_user')
# @mock.patch('google.appengine.api.users.get_current_user')
@mock.patch('framework.users.get_current_user')
def test_dismiss_cue(self, mock_get_current_user):
"""We store the fact that a user has dismissed a cue card."""
mock_get_current_user.return_value = testing_config.Blank(
@ -197,7 +202,8 @@ class UserPrefTest(unittest.TestCase):
self.assertEqual('one@example.com', revised_user_pref.email)
self.assertEqual(['welcome-message'], revised_user_pref.dismissed_cues)
@mock.patch('google.appengine.api.users.get_current_user')
# @mock.patch('google.appengine.api.users.get_current_user')
@mock.patch('framework.users.get_current_user')
def test_dismiss_cue__double(self, mock_get_current_user):
"""We ignore the same user dismissing the same cue multiple times."""
mock_get_current_user.return_value = testing_config.Blank(

Просмотреть файл

@ -29,7 +29,8 @@ from framework import ramcache
from google.appengine.ext import db
from google.appengine.api import mail
import requests
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from google.appengine.ext.webapp.mail_handlers import BounceNotification
from django.template.loader import render_to_string

Просмотреть файл

@ -26,7 +26,8 @@ import werkzeug.exceptions # Flask HTTP stuff.
from google.appengine.ext import db
from google.appengine.api import mail
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from internals import models
from internals import notifier
@ -39,8 +40,8 @@ class EmailFormattingTest(unittest.TestCase):
self.feature_1 = models.Feature(
name='feature one', summary='sum', category=1, visibility=1,
standardization=1, web_dev_views=1, impl_status_chrome=1,
created_by=users.User('creator@example.com'),
updated_by=users.User('editor@example.com'),
created_by=users.User(email='creator@example.com'),
updated_by=users.User(email='editor@example.com'),
blink_components=['Blink'])
self.feature_1.put()
self.component_1 = models.BlinkComponent(name='Blink')
@ -58,8 +59,8 @@ class EmailFormattingTest(unittest.TestCase):
self.feature_2 = models.Feature(
name='feature two', summary='sum', category=1, visibility=1,
standardization=1, web_dev_views=1, impl_status_chrome=1,
created_by=users.User('creator@example.com'),
updated_by=users.User('editor@example.com'),
created_by=users.User(email='creator@example.com'),
updated_by=users.User(email='editor@example.com'),
blink_components=['Blink'])
self.feature_2.put()

Просмотреть файл

@ -26,7 +26,8 @@ from framework import utils
from internals import models
from framework import ramcache
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
class FeaturesJsonHandler(basehandlers.FlaskHandler):

Просмотреть файл

@ -27,7 +27,8 @@ from django import forms
# Appengine imports.
from framework import ramcache
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from google.appengine.ext import db
from framework import basehandlers

Просмотреть файл

@ -21,7 +21,8 @@ from django import forms
from django.core.validators import validate_email
import string
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from internals import models
from internals import processes

Просмотреть файл

@ -16,7 +16,8 @@
from __future__ import division
from __future__ import print_function
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from internals import models
import settings

Просмотреть файл

@ -25,7 +25,8 @@ import os
from framework import permissions
from framework import ramcache
import requests
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from framework import basehandlers
from internals import models

Просмотреть файл

@ -29,7 +29,8 @@ import time
from google.appengine.api import app_identity
from google.appengine.api import memcache
from google.appengine.api import users
# from google.appengine.api import users
from framework import users
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import login_required

Просмотреть файл

@ -29,6 +29,7 @@ limitations under the License.
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="google-signin-client_id" content="77756740465-e5r4o15qg4hkdfiucjpl231o79k3ipjv.apps.googleusercontent.com">
<link rel="apple-touch-icon" href="/static/img/crstatus_128.png">
<link rel="apple-touch-icon-precomposed" href="/static/img/crstatus_128.png">
@ -57,6 +58,66 @@ limitations under the License.
</style>
{% block css %}{% endblock %}
{# Google Platform Library for OAuth #}
<script src="https://apis.google.com/js/platform.js?onload=onLoad" async defer></script>
<script>
function onSignIn(googleUser) {
var profile = googleUser.getBasicProfile();
var id_token = googleUser.getAuthResponse().id_token;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/v0/login');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status == 200) {
window.location.replace(window.location.href.split("?")[0])
}
else {
// signout if cookie not set
signOut();
}
};
let data = JSON.stringify({ "id_token": id_token})
xhr.send(data);
}
function signOut() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/v0/logout');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status == 200) {
console.log('Signed Out' + xhr.responseText);
window.location.reload()
}
};
// let data = JSON.stringify({ "id_token": id_token})
xhr.send();
console.log('User signed out.');
});
}
function onLoad() {
gapi.load('auth2', function() {
gapi.auth2.init();
});
}
function getQueryStringValue (key) {
return decodeURIComponent(window.location.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURIComponent(key).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1"));
}
if (getQueryStringValue("loginStatus") == 'False') {
alert('Please log in.')
}
</script>
{# Loaded immediately because it is used by JS code on the page. #}
<script>
@ -122,6 +183,7 @@ limitations under the License.
{% block js %}{% endblock %}
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-179341418-1"></script>
<script>
(function() {
'use strict';

Просмотреть файл

@ -30,12 +30,16 @@
</a>
<ul>
<li><a href="/settings">Settings</a></li>
<li><a href="{{login.1}}">{{login.0}}</a></li>
<!-- <li><a href="{{login.1}}">{{login.0}}</a></li> -->
<li><a href="#" onclick="signOut()">Sign out</a></li>
</ul>
</div>
{% else %}
<a href="{{login.1}}">{{login.0}}</a>
<!-- <a href="{{login.1}}">{{login.0}}</a> -->
<li><div class="g-signin2" data-onsuccess="onSignIn"></div></li>
{% endif %}
</nav>
</header>