kinto 23.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kinto/__init__.py +92 -0
- kinto/__main__.py +249 -0
- kinto/authorization.py +134 -0
- kinto/config/__init__.py +94 -0
- kinto/config/kinto.tpl +270 -0
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +246 -0
- kinto/core/authentication.py +48 -0
- kinto/core/authorization.py +311 -0
- kinto/core/cache/__init__.py +131 -0
- kinto/core/cache/memcached.py +112 -0
- kinto/core/cache/memory.py +104 -0
- kinto/core/cache/postgresql/__init__.py +178 -0
- kinto/core/cache/postgresql/schema.sql +23 -0
- kinto/core/cache/testing.py +208 -0
- kinto/core/cornice/__init__.py +93 -0
- kinto/core/cornice/cors.py +144 -0
- kinto/core/cornice/errors.py +40 -0
- kinto/core/cornice/pyramidhook.py +373 -0
- kinto/core/cornice/renderer.py +89 -0
- kinto/core/cornice/resource.py +205 -0
- kinto/core/cornice/service.py +641 -0
- kinto/core/cornice/util.py +138 -0
- kinto/core/cornice/validators/__init__.py +94 -0
- kinto/core/cornice/validators/_colander.py +142 -0
- kinto/core/cornice/validators/_marshmallow.py +182 -0
- kinto/core/cornice_swagger/__init__.py +92 -0
- kinto/core/cornice_swagger/converters/__init__.py +21 -0
- kinto/core/cornice_swagger/converters/exceptions.py +6 -0
- kinto/core/cornice_swagger/converters/parameters.py +90 -0
- kinto/core/cornice_swagger/converters/schema.py +249 -0
- kinto/core/cornice_swagger/swagger.py +725 -0
- kinto/core/cornice_swagger/templates/index.html +73 -0
- kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
- kinto/core/cornice_swagger/util.py +42 -0
- kinto/core/cornice_swagger/views.py +78 -0
- kinto/core/decorators.py +74 -0
- kinto/core/errors.py +216 -0
- kinto/core/events.py +301 -0
- kinto/core/initialization.py +738 -0
- kinto/core/listeners/__init__.py +9 -0
- kinto/core/metrics.py +94 -0
- kinto/core/openapi.py +115 -0
- kinto/core/permission/__init__.py +202 -0
- kinto/core/permission/memory.py +167 -0
- kinto/core/permission/postgresql/__init__.py +489 -0
- kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
- kinto/core/permission/postgresql/schema.sql +41 -0
- kinto/core/permission/testing.py +487 -0
- kinto/core/resource/__init__.py +1311 -0
- kinto/core/resource/model.py +412 -0
- kinto/core/resource/schema.py +502 -0
- kinto/core/resource/viewset.py +230 -0
- kinto/core/schema.py +119 -0
- kinto/core/scripts.py +50 -0
- kinto/core/statsd.py +1 -0
- kinto/core/storage/__init__.py +436 -0
- kinto/core/storage/exceptions.py +53 -0
- kinto/core/storage/generators.py +58 -0
- kinto/core/storage/memory.py +651 -0
- kinto/core/storage/postgresql/__init__.py +1131 -0
- kinto/core/storage/postgresql/client.py +120 -0
- kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
- kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
- kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
- kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
- kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
- kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
- kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
- kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
- kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
- kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
- kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
- kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
- kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
- kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
- kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
- kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
- kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
- kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
- kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
- kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
- kinto/core/storage/postgresql/migrator.py +98 -0
- kinto/core/storage/postgresql/pool.py +55 -0
- kinto/core/storage/postgresql/schema.sql +143 -0
- kinto/core/storage/testing.py +1857 -0
- kinto/core/storage/utils.py +37 -0
- kinto/core/testing.py +182 -0
- kinto/core/utils.py +553 -0
- kinto/core/views/__init__.py +0 -0
- kinto/core/views/batch.py +163 -0
- kinto/core/views/errors.py +145 -0
- kinto/core/views/heartbeat.py +106 -0
- kinto/core/views/hello.py +69 -0
- kinto/core/views/openapi.py +35 -0
- kinto/core/views/version.py +50 -0
- kinto/events.py +3 -0
- kinto/plugins/__init__.py +0 -0
- kinto/plugins/accounts/__init__.py +94 -0
- kinto/plugins/accounts/authentication.py +63 -0
- kinto/plugins/accounts/scripts.py +61 -0
- kinto/plugins/accounts/utils.py +13 -0
- kinto/plugins/accounts/views.py +136 -0
- kinto/plugins/admin/README.md +3 -0
- kinto/plugins/admin/VERSION +1 -0
- kinto/plugins/admin/__init__.py +40 -0
- kinto/plugins/admin/build/VERSION +1 -0
- kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
- kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/index.html +18 -0
- kinto/plugins/admin/public/help.html +25 -0
- kinto/plugins/admin/views.py +42 -0
- kinto/plugins/default_bucket/__init__.py +191 -0
- kinto/plugins/flush.py +28 -0
- kinto/plugins/history/__init__.py +65 -0
- kinto/plugins/history/listener.py +181 -0
- kinto/plugins/history/views.py +66 -0
- kinto/plugins/openid/__init__.py +131 -0
- kinto/plugins/openid/utils.py +14 -0
- kinto/plugins/openid/views.py +193 -0
- kinto/plugins/prometheus.py +300 -0
- kinto/plugins/statsd.py +85 -0
- kinto/schema_validation.py +135 -0
- kinto/views/__init__.py +34 -0
- kinto/views/admin.py +195 -0
- kinto/views/buckets.py +45 -0
- kinto/views/collections.py +58 -0
- kinto/views/contribute.py +39 -0
- kinto/views/groups.py +90 -0
- kinto/views/permissions.py +235 -0
- kinto/views/records.py +133 -0
- kinto-23.2.1.dist-info/METADATA +232 -0
- kinto-23.2.1.dist-info/RECORD +142 -0
- kinto-23.2.1.dist-info/WHEEL +5 -0
- kinto-23.2.1.dist-info/entry_points.txt +5 -0
- kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
- kinto-23.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import bcrypt
|
|
2
|
+
from pyramid import authentication as base_auth
|
|
3
|
+
|
|
4
|
+
from kinto.core import utils
|
|
5
|
+
from kinto.core.storage import exceptions as storage_exceptions
|
|
6
|
+
|
|
7
|
+
from .utils import (
|
|
8
|
+
ACCOUNT_CACHE_KEY,
|
|
9
|
+
ACCOUNT_POLICY_NAME,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def account_check(username, password, request):
|
|
14
|
+
settings = request.registry.settings
|
|
15
|
+
hmac_secret = settings["userid_hmac_secret"]
|
|
16
|
+
cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username))
|
|
17
|
+
cache_ttl = int(settings.get("account_cache_ttl_seconds", 30))
|
|
18
|
+
hashed_password = utils.hmac_digest(cache_key, password)
|
|
19
|
+
|
|
20
|
+
# Check cache to see whether somebody has recently logged in with the same
|
|
21
|
+
# username and password.
|
|
22
|
+
cache = request.registry.cache
|
|
23
|
+
cache_result = cache.get(cache_key)
|
|
24
|
+
|
|
25
|
+
# Username and password have been verified previously. No need to compare hashes
|
|
26
|
+
if cache_result == hashed_password:
|
|
27
|
+
# Refresh the cache TTL.
|
|
28
|
+
cache.expire(cache_key, cache_ttl)
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
# Back to standard procedure
|
|
32
|
+
parent_id = username
|
|
33
|
+
try:
|
|
34
|
+
existing = request.registry.storage.get(
|
|
35
|
+
parent_id=parent_id, resource_name="account", object_id=username
|
|
36
|
+
)
|
|
37
|
+
except storage_exceptions.ObjectNotFoundError:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
hashed = existing["password"].encode(encoding="utf-8")
|
|
41
|
+
pwd_str = password.encode(encoding="utf-8")
|
|
42
|
+
# Check if password is valid (it is a very expensive computation)
|
|
43
|
+
if bcrypt.checkpw(pwd_str, hashed):
|
|
44
|
+
cache.set(cache_key, hashed_password, ttl=cache_ttl)
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AccountsAuthenticationPolicy(base_auth.BasicAuthAuthenticationPolicy):
|
|
49
|
+
"""Accounts authentication policy.
|
|
50
|
+
|
|
51
|
+
It will check that the credentials exist in the account resource.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name = ACCOUNT_POLICY_NAME
|
|
55
|
+
|
|
56
|
+
def __init__(self, *args, **kwargs):
|
|
57
|
+
super().__init__(account_check, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def effective_principals(self, request):
|
|
60
|
+
# Bypass default Pyramid construction of principals because
|
|
61
|
+
# Pyramid multiauth already adds userid, Authenticated and Everyone
|
|
62
|
+
# principals.
|
|
63
|
+
return []
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import transaction as current_transaction
|
|
5
|
+
from pyramid.settings import asbool
|
|
6
|
+
|
|
7
|
+
from .utils import hash_password
|
|
8
|
+
from .views import AccountIdGenerator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_user(env, username=None, password=None):
|
|
15
|
+
"""Administrative command to create a new user."""
|
|
16
|
+
registry = env["registry"]
|
|
17
|
+
settings = registry.settings
|
|
18
|
+
readonly_mode = asbool(settings.get("readonly", False))
|
|
19
|
+
if readonly_mode:
|
|
20
|
+
message = "Cannot create a user with a readonly server."
|
|
21
|
+
logger.error(message)
|
|
22
|
+
return 51
|
|
23
|
+
|
|
24
|
+
if "kinto.plugins.accounts" not in settings["includes"]:
|
|
25
|
+
message = "Cannot create a user when the accounts plugin is not installed."
|
|
26
|
+
logger.error(message)
|
|
27
|
+
return 52
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
validator = AccountIdGenerator()
|
|
31
|
+
if username is None:
|
|
32
|
+
username = input("Username: ")
|
|
33
|
+
while not validator.match(username):
|
|
34
|
+
print("{} is not a valid username.")
|
|
35
|
+
print(f"Username should match {validator.regexp}, please try again.")
|
|
36
|
+
username = input("Username: ")
|
|
37
|
+
|
|
38
|
+
if password is None:
|
|
39
|
+
while True: # The user didn't entered twice the same password
|
|
40
|
+
password = getpass.getpass(f"Please enter a password for {username}: ")
|
|
41
|
+
confirm = getpass.getpass("Please confirm the password: ")
|
|
42
|
+
|
|
43
|
+
if password == confirm:
|
|
44
|
+
break
|
|
45
|
+
print("Sorry, passwords do not match, please try again.")
|
|
46
|
+
except EOFError:
|
|
47
|
+
print("User creation aborted")
|
|
48
|
+
return 53
|
|
49
|
+
|
|
50
|
+
print(f"Creating user '{username}'")
|
|
51
|
+
entry = {"id": username, "password": hash_password(password)}
|
|
52
|
+
registry.storage.update(
|
|
53
|
+
resource_name="account", parent_id=username, object_id=username, obj=entry
|
|
54
|
+
)
|
|
55
|
+
registry.permission.add_principal_to_ace(
|
|
56
|
+
f"/accounts/{username}", "write", f"account:{username}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
current_transaction.commit()
|
|
60
|
+
|
|
61
|
+
return 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import bcrypt
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
ACCOUNT_CACHE_KEY = "accounts:{}:verified"
|
|
5
|
+
ACCOUNT_POLICY_NAME = "account"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def hash_password(password):
|
|
9
|
+
# Store password safely in database as str
|
|
10
|
+
# (bcrypt.hashpw returns base64 bytes).
|
|
11
|
+
pwd_str = password.encode(encoding="utf-8")
|
|
12
|
+
hashed = bcrypt.hashpw(pwd_str, bcrypt.gensalt())
|
|
13
|
+
return hashed.decode(encoding="utf-8")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import colander
|
|
2
|
+
from pyramid import httpexceptions
|
|
3
|
+
from pyramid.authorization import Authenticated, Everyone
|
|
4
|
+
from pyramid.decorator import reify
|
|
5
|
+
from pyramid.events import subscriber
|
|
6
|
+
from pyramid.settings import aslist
|
|
7
|
+
|
|
8
|
+
from kinto.core import resource, utils
|
|
9
|
+
from kinto.core.errors import http_error, raise_invalid
|
|
10
|
+
from kinto.core.events import ACTIONS, ResourceChanged
|
|
11
|
+
from kinto.views import NameGenerator
|
|
12
|
+
|
|
13
|
+
from .utils import ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME, hash_password
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_posted_body_id(request):
|
|
17
|
+
try:
|
|
18
|
+
# Anonymous creation with POST.
|
|
19
|
+
return request.json["data"]["id"]
|
|
20
|
+
except (ValueError, KeyError):
|
|
21
|
+
# Bad POST data.
|
|
22
|
+
if request.method.lower() == "post":
|
|
23
|
+
error_details = {"name": "data.id", "description": "data.id in body: Required"}
|
|
24
|
+
raise_invalid(request, **error_details)
|
|
25
|
+
# Anonymous GET
|
|
26
|
+
error_msg = "Cannot read accounts."
|
|
27
|
+
raise http_error(httpexceptions.HTTPUnauthorized(), error=error_msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AccountIdGenerator(NameGenerator):
|
|
31
|
+
"""Allow @ signs in account IDs."""
|
|
32
|
+
|
|
33
|
+
regexp = r"^[a-zA-Z0-9][+.@a-zA-Z0-9_-]*$"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AccountSchema(resource.ResourceSchema):
|
|
37
|
+
password = colander.SchemaNode(colander.String())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@resource.register()
|
|
41
|
+
class Account(resource.Resource):
|
|
42
|
+
schema = AccountSchema
|
|
43
|
+
|
|
44
|
+
def __init__(self, request, context):
|
|
45
|
+
settings = request.registry.settings
|
|
46
|
+
# Store if current user is administrator (before accessing get_parent_id())
|
|
47
|
+
allowed_from_settings = settings.get("account_write_principals", [])
|
|
48
|
+
context.is_administrator = (
|
|
49
|
+
len(set(aslist(allowed_from_settings)) & set(request.prefixed_principals)) > 0
|
|
50
|
+
)
|
|
51
|
+
# Shortcut to check if current is anonymous (before get_parent_id()).
|
|
52
|
+
context.is_anonymous = Authenticated not in request.effective_principals
|
|
53
|
+
|
|
54
|
+
super().__init__(request, context)
|
|
55
|
+
|
|
56
|
+
# Overwrite the current principal set by Resource.
|
|
57
|
+
if self.model.current_principal == Everyone or context.is_administrator:
|
|
58
|
+
# Creation is anonymous, but author with write perm is this:
|
|
59
|
+
self.model.current_principal = f"{ACCOUNT_POLICY_NAME}:{self.model.parent_id}"
|
|
60
|
+
|
|
61
|
+
@reify
|
|
62
|
+
def id_generator(self):
|
|
63
|
+
# This generator is used for ID validation.
|
|
64
|
+
return AccountIdGenerator()
|
|
65
|
+
|
|
66
|
+
def get_parent_id(self, request):
|
|
67
|
+
# The whole challenge here is that we want to isolate what
|
|
68
|
+
# authenticated users can list, but give access to everything to
|
|
69
|
+
# administrators.
|
|
70
|
+
# Plus when anonymous create accounts, we have to set their parent id
|
|
71
|
+
# to the same value they would obtain when authenticated.
|
|
72
|
+
if self.context.is_administrator:
|
|
73
|
+
if self.context.on_plural_endpoint:
|
|
74
|
+
# Accounts created by admin should have userid as parent.
|
|
75
|
+
if request.method.lower() == "post":
|
|
76
|
+
return _extract_posted_body_id(request)
|
|
77
|
+
else:
|
|
78
|
+
# Admin see all accounts.
|
|
79
|
+
return "*"
|
|
80
|
+
else:
|
|
81
|
+
# No pattern matching for admin on single record.
|
|
82
|
+
return request.matchdict["id"]
|
|
83
|
+
|
|
84
|
+
if not self.context.is_anonymous:
|
|
85
|
+
# Authenticated users see their own account only.
|
|
86
|
+
return request.selected_userid
|
|
87
|
+
|
|
88
|
+
# Anonymous creation with PUT.
|
|
89
|
+
if "id" in request.matchdict:
|
|
90
|
+
return request.matchdict["id"]
|
|
91
|
+
|
|
92
|
+
return _extract_posted_body_id(request)
|
|
93
|
+
|
|
94
|
+
def process_object(self, new, old=None):
|
|
95
|
+
new = super(Account, self).process_object(new, old)
|
|
96
|
+
|
|
97
|
+
if "data" in self.request.json and "password" in self.request.json["data"]:
|
|
98
|
+
new["password"] = hash_password(new["password"])
|
|
99
|
+
|
|
100
|
+
# Do not let accounts be created without usernames.
|
|
101
|
+
if self.model.id_field not in new:
|
|
102
|
+
error_details = {"name": "data.id", "description": "Accounts must have an ID."}
|
|
103
|
+
raise_invalid(self.request, **error_details)
|
|
104
|
+
|
|
105
|
+
# Administrators can reach other accounts and anonymous have no
|
|
106
|
+
# selected_userid. So do not try to enforce.
|
|
107
|
+
if self.context.is_administrator or self.context.is_anonymous:
|
|
108
|
+
return new
|
|
109
|
+
|
|
110
|
+
# Otherwise, we force the id to match the authenticated username.
|
|
111
|
+
if new[self.model.id_field] != self.request.selected_userid:
|
|
112
|
+
error_details = {
|
|
113
|
+
"name": "data.id",
|
|
114
|
+
"description": "Username and account ID do not match.",
|
|
115
|
+
}
|
|
116
|
+
raise_invalid(self.request, **error_details)
|
|
117
|
+
|
|
118
|
+
return new
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Clear cache on account change
|
|
122
|
+
@subscriber(
|
|
123
|
+
ResourceChanged, for_resources=("account",), for_actions=(ACTIONS.UPDATE, ACTIONS.DELETE)
|
|
124
|
+
)
|
|
125
|
+
def on_account_changed(event):
|
|
126
|
+
request = event.request
|
|
127
|
+
cache = request.registry.cache
|
|
128
|
+
settings = request.registry.settings
|
|
129
|
+
hmac_secret = settings["userid_hmac_secret"]
|
|
130
|
+
|
|
131
|
+
for obj in event.impacted_objects:
|
|
132
|
+
# Extract username and password from current user
|
|
133
|
+
username = obj["old"]["id"]
|
|
134
|
+
cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username))
|
|
135
|
+
# Delete cache
|
|
136
|
+
cache.delete(cache_key)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5.0.1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pyramid.httpexceptions import HTTPTemporaryRedirect
|
|
4
|
+
from pyramid.static import static_view
|
|
5
|
+
|
|
6
|
+
from .views import admin_home_view
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def includeme(config):
|
|
10
|
+
admin_assets_path = config.registry.settings["admin_assets_path"]
|
|
11
|
+
if not admin_assets_path:
|
|
12
|
+
# Use bundled admin.
|
|
13
|
+
admin_assets_path = "kinto.plugins.admin:build"
|
|
14
|
+
version_file_parent = Path(__file__).parent
|
|
15
|
+
else:
|
|
16
|
+
version_file_parent = Path(admin_assets_path)
|
|
17
|
+
|
|
18
|
+
admin_version = (version_file_parent / "VERSION").read_text().strip()
|
|
19
|
+
|
|
20
|
+
# Expose capability.
|
|
21
|
+
config.add_api_capability(
|
|
22
|
+
"admin",
|
|
23
|
+
version=admin_version,
|
|
24
|
+
description="Serves the admin console.",
|
|
25
|
+
url="https://github.com/Kinto/kinto-admin/",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
config.add_route("admin_home", "/admin/")
|
|
29
|
+
config.add_view(admin_home_view, route_name="admin_home")
|
|
30
|
+
|
|
31
|
+
build_dir = static_view(admin_assets_path, use_subpath=True)
|
|
32
|
+
config.add_route("catchall_static", "/admin/*subpath")
|
|
33
|
+
config.add_view(build_dir, route_name="catchall_static")
|
|
34
|
+
|
|
35
|
+
# Setup redirect without trailing slash.
|
|
36
|
+
def admin_redirect_view(request):
|
|
37
|
+
raise HTTPTemporaryRedirect(request.path + "/")
|
|
38
|
+
|
|
39
|
+
config.add_route("admin_redirect", "/admin")
|
|
40
|
+
config.add_view(admin_redirect_view, route_name="admin_redirect")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5.0.1
|