descope / django-descope Goto Github PK
View Code? Open in Web Editor NEWDescope plugin for Django framework
Home Page: https://docs.descope.com
License: MIT License
Descope plugin for Django framework
Home Page: https://docs.descope.com
License: MIT License
Big bug and a pretty frustrating one..
https://github.com/descope/django-descope/blob/main/django_descope/middleware.py#L17
All views get called 2X if using the descope middleware.
I haven't tested this exhaustively, but it seems like this is somewhat misleading. The Descope Middleware needs to go after a bunch of things, not just the SessionMiddleware. I would suggest stating that it should go at the end, as the errors you get is far from obvious that that's the problem.
Upon doing pip3 install django-descope.
I get the error:
ModuleNotFoundError: No module named 'debug_toolbar'
I resolved by doing (https://bobbyhadz.com/blog/python-no-module-named-debug-toolbar):
pip3 install django-debug-toolbar
Maybe missing dependency?
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
Renovate tried to run on this repository, but found these problems.
These updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox.
These updates are pending. To force PRs open, click the checkbox below.
.github/workflows/ci.yaml
actions/checkout v4
actions/setup-python v5
actions/cache v4
.github/workflows/python-publish.yaml
actions/checkout v4
pyproject.toml
poetry >=1,<2
pyproject.toml
python >=3.8.1,<4.0
Django >=3.2.19,<5.1
descope ^1.5.1
flake8 7.0.0
black 23.11.0
pre-commit 3.6.0
liccheck 0.9.1
isort 5.13.0
python-dotenv 1.0.1
tox 4.11.4
django-debug-toolbar 4.3.0
django-stubs 4.2.7
For some reason, request.session.get("user") == None
inside of a view, even though I am authenticated.
I am able to sign in just fine using the scope, and logging out works as well, and Django Admin works, but the test
endpoint doesn't work, I always get {}
along with these logs:
dev-retrieve-front-end-1 | Validating (and refreshing) Descope session
dev-retrieve-front-end-1 | session eyJhbGciOiJSUzI1NiIsImtpZCI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJ0eXAiOiJKV1QifQ.eyJhbXIiOlsid2ViYXV0aG4iXSwiZHJuIjoiRFMiLCJleHAiOjE2ODU5MzI2NDUsImlhdCI6MTY4NTkzMjA0NSwiaXNzIjoiUDJPbm1SZ3lIUU03dE1FTUU4bWpQR1hyZ0tjaSIsInJleHAiOiIyMDIzLTA3LTAzVDAyOjI3OjI1WiIsInJvbGVzIjpbImlzX3N0YWZmIiwiaXNfc3VwZXJ1c2VyIl0sInN1YiI6IlUyT25tbTJROVVSNWVGRlBNU3NZU0VsNHdXVjcifQ.Wc9RB4Qf3ZzdkD4I4oq_d7SgH5gw54Qq4cI-u8j3T3-BSnx-s6GV1Jz13VF07fZnzYSnV_jJRM-FnMqwL89CmdqvTrOHUSA8FDkJygthgFelHwE2_OJWFGePf4T-V6zeqWx427uoNIQFHQvbMelRaHH__dc66NbOzILZWcafx5VAdLIj5IqX-MZqVecUMiZqMTEw8q1hCbqX6lHrhZUrN_Ez47Exc173WLxd2AVy3El-OS5s6rP6y4FFXza7oUranJmEuoRXsPL_DShHV2fpnuQC3bL-gRKgY03RB3LPq7O-_VxeHIkbMDSbyFUefKP6WL05od9U_fklEwyQUj4WgQ
dev-retrieve-front-end-1 | refresh eyJhbGciOiJSUzI1NiIsImtpZCI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJ0eXAiOiJKV1QifQ.eyJhbXIiOlsid2ViYXV0aG4iXSwiZHJuIjoiRFNSIiwiZXhwIjoxNjg4MzUxMjQ1LCJpYXQiOjE2ODU5MzIwNDUsImlzcyI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJzdWIiOiJVMk9ubW0yUTlVUjVlRkZQTVNzWVNFbDR3V1Y3In0.U-T8qqZ-beD2TDBjtAhNuC6c2OERoekI1_8sAC9KAxr0aPq6-dGu0jJb8Nqu2lBE5R_6gaK9n2nI01j3QquWBdbhsePzcp_TVTr5GDTHYEjxMQxbXOQycfj5zmPVdJKAfd0f4GsQjOHLW-PEmX911uB5T98WIPGX2SO67eWjaOLX8Aw1DiNMqRNhA-5fOPeEXtDsP82n28e9yQU6Kv8sMbkHXp28Tmm4PsP-dPha6nGlslouY1yESofwObL_EO2jSo19Db0PIwY-0UUX3bXcLAVOMNc2knucxH6iN5cOADk5HS_Jg_ztwugp7c2ADgG4yOYBXvwAbhz8ve_AGeRzfw
dev-retrieve-front-end-1 | Starting new HTTPS connection (1): api.descope.com:443
dev-retrieve-front-end-1 | https://api.descope.com:443 "GET /v2/keys/P2OnmRgyHQM7tMEME8mjPGXrgKci HTTP/1.1" 200 None
dev-retrieve-front-end-1 | {'amr': ['webauthn'], 'drn': 'DS', 'exp': 1685932645, 'iat': 1685932045, 'iss': 'P2OnmRgyHQM7tMEME8mjPGXrgKci', 'rexp': '2023-07-03T02:27:25Z', 'roles': ['is_staff', 'is_superuser'], 'sub': 'U2Onmm2Q9UR5eFFPMSsYSEl4wWV7', 'jwt': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJ0eXAiOiJKV1QifQ.eyJhbXIiOlsid2ViYXV0aG4iXSwiZHJuIjoiRFMiLCJleHAiOjE2ODU5MzI2NDUsImlhdCI6MTY4NTkzMjA0NSwiaXNzIjoiUDJPbm1SZ3lIUU03dE1FTUU4bWpQR1hyZ0tjaSIsInJleHAiOiIyMDIzLTA3LTAzVDAyOjI3OjI1WiIsInJvbGVzIjpbImlzX3N0YWZmIiwiaXNfc3VwZXJ1c2VyIl0sInN1YiI6IlUyT25tbTJROVVSNWVGRlBNU3NZU0VsNHdXVjcifQ.Wc9RB4Qf3ZzdkD4I4oq_d7SgH5gw54Qq4cI-u8j3T3-BSnx-s6GV1Jz13VF07fZnzYSnV_jJRM-FnMqwL89CmdqvTrOHUSA8FDkJygthgFelHwE2_OJWFGePf4T-V6zeqWx427uoNIQFHQvbMelRaHH__dc66NbOzILZWcafx5VAdLIj5IqX-MZqVecUMiZqMTEw8q1hCbqX6lHrhZUrN_Ez47Exc173WLxd2AVy3El-OS5s6rP6y4FFXza7oUranJmEuoRXsPL_DShHV2fpnuQC3bL-gRKgY03RB3LPq7O-_VxeHIkbMDSbyFUefKP6WL05od9U_fklEwyQUj4WgQ', 'sessionToken': {'amr': ['webauthn'], 'drn': 'DS', 'exp': 1685932645, 'iat': 1685932045, 'iss': 'P2OnmRgyHQM7tMEME8mjPGXrgKci', 'rexp': '2023-07-03T02:27:25Z', 'roles': ['is_staff', 'is_superuser'], 'sub': 'U2Onmm2Q9UR5eFFPMSsYSEl4wWV7', 'jwt': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJ0eXAiOiJKV1QifQ.eyJhbXIiOlsid2ViYXV0aG4iXSwiZHJuIjoiRFMiLCJleHAiOjE2ODU5MzI2NDUsImlhdCI6MTY4NTkzMjA0NSwiaXNzIjoiUDJPbm1SZ3lIUU03dE1FTUU4bWpQR1hyZ0tjaSIsInJleHAiOiIyMDIzLTA3LTAzVDAyOjI3OjI1WiIsInJvbGVzIjpbImlzX3N0YWZmIiwiaXNfc3VwZXJ1c2VyIl0sInN1YiI6IlUyT25tbTJROVVSNWVGRlBNU3NZU0VsNHdXVjcifQ.Wc9RB4Qf3ZzdkD4I4oq_d7SgH5gw54Qq4cI-u8j3T3-BSnx-s6GV1Jz13VF07fZnzYSnV_jJRM-FnMqwL89CmdqvTrOHUSA8FDkJygthgFelHwE2_OJWFGePf4T-V6zeqWx427uoNIQFHQvbMelRaHH__dc66NbOzILZWcafx5VAdLIj5IqX-MZqVecUMiZqMTEw8q1hCbqX6lHrhZUrN_Ez47Exc173WLxd2AVy3El-OS5s6rP6y4FFXza7oUranJmEuoRXsPL_DShHV2fpnuQC3bL-gRKgY03RB3LPq7O-_VxeHIkbMDSbyFUefKP6WL05od9U_fklEwyQUj4WgQ'}, 'permissions': [], 'tenants': {}, 'projectId': 'P2OnmRgyHQM7tMEME8mjPGXrgKci', 'userId': 'U2Onmm2Q9UR5eFFPMSsYSEl4wWV7'}
dev-retrieve-front-end-1 | Starting new HTTPS connection (1): api.descope.com:443
dev-retrieve-front-end-1 | https://api.descope.com:443 "GET /v1/auth/me HTTP/1.1" 200 None
dev-retrieve-front-end-1 | Test view called
dev-retrieve-front-end-1 | None
Note None
being returned for logger.info(request.session.get("user"))
despite the logs for Descope clearly having tokens.
If I do logger.info(request.session.items())
then I get:
dict_items([('DS', None), ('DSR', 'eyJhbGciOiJSUzI1NiIsImtpZCI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJ0eXAiOiJKV1QifQ.eyJhbXIiOlsid2ViYXV0aG4iXSwiZHJuIjoiRFNSIiwiZXhwIjoxNjg4MzUxMjQ1LCJpYXQiOjE2ODU5MzIwNDUsImlzcyI6IlAyT25tUmd5SFFNN3RNRU1FOG1qUEdYcmdLY2kiLCJzdWIiOiJVMk9ubW0yUTlVUjVlRkZQTVNzWVNFbDR3V1Y3In0.U-T8qqZ-beD2TDBjtAhNuC6c2OERoekI1_8sAC9KAxr0aPq6-dGu0jJb8Nqu2lBE5R_6gaK9n2nI01j3QquWBdbhsePzcp_TVTr5GDTHYEjxMQxbXOQycfj5zmPVdJKAfd0f4GsQjOHLW-PEmX911uB5T98WIPGX2SO67eWjaOLX8Aw1DiNMqRNhA-5fOPeEXtDsP82n28e9yQU6Kv8sMbkHXp28Tmm4PsP-dPha6nGlslouY1yESofwObL_EO2jSo19Db0PIwY-0UUX3bXcLAVOMNc2knucxH6iN5cOADk5HS_Jg_ztwugp7c2ADgG4yOYBXvwAbhz8ve_AGeRzfw'), ('_auth_user_id', '2'), ('_auth_user_backend', 'django_descope.authentication.DescopeAuthentication'), ('_auth_user_hash', '6f7fd439db311a2ac4942e643d5f1e7f91df4aafd89b348d8a0d44bf68cc243e')])
import logging
from django.contrib.auth import logout
from django.http import HttpRequest, HttpResponseRedirect, JsonResponse
from django.urls import reverse
from django.views import View
logger = logging.getLogger(__name__)
class Logout(View):
def get(self, request: HttpRequest):
logout(request)
return HttpResponseRedirect(reverse("index"))
class Test(View):
def get(self, request: HttpRequest):
logger.info("Test view called")
logger.info(request.session.get("user"))
return JsonResponse(request.session.get("user", {}))
from django.urls import path
from django.views.generic import TemplateView
from .views import Test
urlpatterns = [
path(
"",
TemplateView.as_view(template_name="descope_login.html"),
name="index",
),
path("test", Test.as_view(), name="test"),
]
{% load descope %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Descope Django</title>
<meta name="description" content="Descope Example" />
<meta name="author" content="Descope" />
</head>
<body>
{% if user.is_authenticated %}
<h1>Welcome {{ user.email }} you are logged in!</h1>
<p><a href="{% url 'logout' %}">Log Out</a></p>
{% else %} {% descope_flow "sign-up-or-in" "/" %} {% endif %}
</body>
</html>
from pathlib import Path
import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
APPS_DIR = BASE_DIR / "app"
env = environ.Env()
environ.Env.read_env(BASE_DIR / ".env")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
DJANGO_ENV = env.str("DJANGO_ENV", "dev")
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env("DJANGO_DEBUG", default=False)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS")
# Application definition
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.humanize",
]
THIRD_PARTY_APPS = [
"django_extensions",
"django_htmx",
"django_descope",
# "logging_middleware",
]
if DEBUG:
THIRD_PARTY_APPS += [
"django_browser_reload",
"debug_toolbar",
"django_fastdev",
]
LOCAL_APPS = ["app"]
INSTALLED_APPS = LOCAL_APPS + THIRD_PARTY_APPS + DJANGO_APPS
AUTHENTICATION_BACKENDS = [
"django_descope.authentication.DescopeAuthentication"
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
# "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_htmx.middleware.HtmxMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
# "logging_middleware.middlewares.DjangoLoggingMiddleware",
"django_descope.middleware.DescopeMiddleware",
]
if DEBUG:
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django_browser_reload.middleware.BrowserReloadMiddleware",
]
DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
"SHOW_TEMPLATE_CONTEXT": True,
}
ROOT_URLCONF = "urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "app.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"OPTIONS": {"options": "-c search_path=django"},
"NAME": env.str("POSTGRES_DB"),
"USER": env.str("POSTGRES_USER"),
"PASSWORD": env.str("POSTGRES_PASSWORD"),
"HOST": "postgres",
"PORT": "5432",
}
}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
MEDIA_ROOT = str(APPS_DIR / "media")
MEDIA_URL = "/media/"
AUTH_PASSWORD_VALIDATORS: list = (
[]
) # With descope, there's no need for passwords!
if DJANGO_ENV == "production":
# Security
CSRF_TRUSTED_ORIGINS = env.list("DJANGO_CSRF_TRUSTED_ORIGINS")
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# TODO: ssl
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
},
}
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Descope
DESCOPE_PROJECT_ID = env.str("DESCOPE_PROJECT_ID")
There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.
Error type: Cannot find preset's package (local>descope/renovate-config)
Our current login process works great with release 1.3.0, but when upgrading to 1.3.1 we get the following error in the Django container logs and the user is not authenticated. Confirmed that rebuilding our containers with release 1.3.0 fixes the problem (no errors and user logs in successfully). Note that pytest (using descope test user) passes authentication with both releases.
unified_platform-portal-1 | [18/Dec/2023 13:58:50] "GET /?descope-login-flow=development-sign-up-or-iin%7C%23%7C2ZibWdAzcWHsKstpOyINKinigEx_11.end&t=093739ad7efb0a74bc889fb0cbc5a32e770dc709b6de189deed6453167ea5c3d HTTP/1.1" 200 4833
unified_platform-portal-1 | Internal Server Error: /auth/store_jwt
unified_platform-portal-1 | Traceback (most recent call last):
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
unified_platform-portal-1 | response = get_response(request)
unified_platform-portal-1 | ^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
unified_platform-portal-1 | response = wrapped_callback(request, *callback_args, **callback_kwargs)
unified_platform-portal-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django/views/generic/base.py", line 104, in view
unified_platform-portal-1 | return self.dispatch(request, *args, **kwargs)
unified_platform-portal-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 46, in _wrapper
unified_platform-portal-1 | return bound_method(*args, **kwargs)
unified_platform-portal-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django/views/decorators/cache.py", line 62, in _wrapper_view_func
unified_platform-portal-1 | response = view_func(request, *args, **kwargs)
unified_platform-portal-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django/views/generic/base.py", line 143, in dispatch
unified_platform-portal-1 | return handler(request, *args, **kwargs)
unified_platform-portal-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django_descope/views.py", line 23, in post
unified_platform-portal-1 | add_tokens_to_request(request, session, refresh)
unified_platform-portal-1 | File "/usr/local/lib/python3.11/site-packages/django_descope/authentication.py", line 18, in add_tokens_to_request
unified_platform-portal-1 | session[SESSION_COOKIE_NAME] = session_token
unified_platform-portal-1 | ~~~~~~~^^^^^^^^^^^^^^^^^^^^^
unified_platform-portal-1 | TypeError: 'WSGIRequest' object does not support item assignment
unified_platform-portal-1 | [18/Dec/2023 13:58:50] "POST /auth/store_jwt HTTP/1.1" 500 88472
It's not entirely clear to me what to do once the user is logged in using django-descope
. For instance, looking at: https://github.com/descope/python-sdk/blob/main/samples/access_key_sample_app.py as an example, how would one do:
permission_name = "TestPermission"
permission_presented = descope_client.validate_permissions(
jwt_response, [permission_name]
)
logging.info(
f"{permission_name} presented on the jwt: [{permission_presented}]"
)
role_name = "TestRole"
role_presented = descope_client.validate_roles(jwt_response, [role_name])
logging.info(f"{role_name} presented
using django-descope
? Should I be using the Descope Python SDK directly, and use DescopeClient
separately from django-descope
, or is there a better way?
I am not sure how to use the Django Admin with Descope. Can the example and quickstart guide be extended to explain how?
Given that I have a custom user model, and an associated model like so:
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
class Account(models.Model):
name = models.CharField(max_length=200)
descope_tenant = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.name
class User(AbstractBaseUser):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
This creates a User
model, with a non-nullable field relating it to the Account
model, meaning that on creation of a User
, an account MUST be provided. In my scenario, an Account
is synonymous with a Descope tenant, and it stores the descope tenant ID.
This association can easily be created by getting the tenant the user belongs to(in this scenario, a user could never be assigned to more than one tenant) during the Django user creation and determining the Account
for it, and providing that to the foreign key on the user.
As it stands, I don't see a way to customize the user creation process within django-descope. The Django user is created during the get_user
function in django-descope with the get_or_create
function and is just passed the username.
One possible solution might be to allow the user to define a callable(maybe via Django's settings.py
or some function to register it?) which would get called from this function that is expected to work the same as the existing get_or_create
call. There definitely may be a better solution, this was just one possibility I thought of.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.