splent-feature-apikeys 0.1.1__tar.gz

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.
Files changed (29) hide show
  1. splent_feature_apikeys-0.1.1/MANIFEST.in +8 -0
  2. splent_feature_apikeys-0.1.1/PKG-INFO +129 -0
  3. splent_feature_apikeys-0.1.1/README.md +123 -0
  4. splent_feature_apikeys-0.1.1/pyproject.toml +75 -0
  5. splent_feature_apikeys-0.1.1/setup.cfg +4 -0
  6. splent_feature_apikeys-0.1.1/src/splent_feature_apikeys.egg-info/PKG-INFO +129 -0
  7. splent_feature_apikeys-0.1.1/src/splent_feature_apikeys.egg-info/SOURCES.txt +27 -0
  8. splent_feature_apikeys-0.1.1/src/splent_feature_apikeys.egg-info/dependency_links.txt +1 -0
  9. splent_feature_apikeys-0.1.1/src/splent_feature_apikeys.egg-info/top_level.txt +1 -0
  10. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/__init__.py +35 -0
  11. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/config.py +44 -0
  12. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/decorators.py +130 -0
  13. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/forms.py +42 -0
  14. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/hooks.py +16 -0
  15. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/migrations/alembic.ini +36 -0
  16. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/migrations/env.py +9 -0
  17. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/migrations/script.py.mako +24 -0
  18. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/migrations/versions/089274640134_splent_feature_apikeys.py +53 -0
  19. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/models.py +103 -0
  20. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/repositories.py +27 -0
  21. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/routes.py +322 -0
  22. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/services.py +180 -0
  23. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/templates/apikeys/created.html +48 -0
  24. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/templates/apikeys/index.html +97 -0
  25. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/templates/apikeys/new.html +62 -0
  26. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/templates/hooks/sidebar_tokens.html +5 -0
  27. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/throttle.py +191 -0
  28. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/translations/es/LC_MESSAGES/messages.mo +0 -0
  29. splent_feature_apikeys-0.1.1/src/splent_io/splent_feature_apikeys/translations/es/LC_MESSAGES/messages.po +204 -0
@@ -0,0 +1,8 @@
1
+ # Jinja templates (feature views + hook fragments)
2
+ recursive-include src/splent_io/splent_feature_apikeys/templates *.html
3
+
4
+ # Alembic migration config and scripts
5
+ recursive-include src/splent_io/splent_feature_apikeys/migrations *.py *.ini *.mako
6
+
7
+ # Compiled translation catalogs
8
+ recursive-include src/splent_io/splent_feature_apikeys/translations *.mo *.po
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_apikeys
3
+ Version: 0.1.1
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
6
+
7
+ # splent_feature_apikeys
8
+
9
+ Developer API tokens for a SPLENT product. A port of the UVLHub `apikeys`
10
+ feature to SPLENT conventions, and the only place in a product where a
11
+ password is exchanged for a credential a machine can carry.
12
+
13
+ It owns tokens and nothing else. It does not know what the tokens are used
14
+ for, and it never touches the `auth` feature: credentials are checked through
15
+ the `AuthenticationService` that auth already registers.
16
+
17
+ ## The model
18
+
19
+ `ApiToken` belongs to one user and keeps:
20
+
21
+ | Column | Meaning |
22
+ |----------------|------------------------------------------------------------|
23
+ | `user_id` | Owner, foreign key to auth's `user.id` |
24
+ | `name` | Human label chosen by the owner |
25
+ | `token_hash` | sha256 of the plaintext, hex. The plaintext is never stored |
26
+ | `prefix` | First 15 characters of the plaintext, indexed, safe to show |
27
+ | `scopes` | Comma separated, as in UVLHub |
28
+ | `created_at`, `expires_at`, `revoked_at`, `last_used_at` | lifecycle |
29
+
30
+ A plaintext token looks like `splent_<64 hex characters>`. The `splent_`
31
+ label makes a leaked token greppable; the prefix makes verification a single
32
+ indexed lookup instead of a table scan.
33
+
34
+ ## Scopes
35
+
36
+ `read_spl` and `write_spl`. Unknown names are dropped, and a request that
37
+ asks for nothing but unknown names is refused rather than silently producing
38
+ a token that can do nothing.
39
+
40
+ ## The service
41
+
42
+ `ApiTokenService`, registered under that name in the service locator.
43
+
44
+ ```python
45
+ api_token_service = service_proxy("ApiTokenService")
46
+
47
+ row, plaintext = api_token_service.mint(user, "laptop", ["write_spl"], ttl_days=90)
48
+ api_token_service.verify(plaintext) # row, or None if unknown/revoked/expired
49
+ api_token_service.find(plaintext) # row whatever its state, to explain a refusal
50
+ api_token_service.revoke(row.id, user) # only the owner can revoke
51
+ api_token_service.touch(row) # last_used_at, at most once every 5 minutes
52
+ api_token_service.authenticate(email, password) # (user, None) or (None, reason)
53
+ ```
54
+
55
+ `mint` returns the plaintext once. Nothing else in the product can ever
56
+ produce it again.
57
+
58
+ ## Guarding a route
59
+
60
+ ```python
61
+ from splent_io.splent_feature_apikeys.decorators import require_token
62
+
63
+ @some_bp.route("/api/v1/spls", methods=["POST"])
64
+ @require_token("write_spl")
65
+ def publish():
66
+ g.api_user # the owner
67
+ g.api_token # the presenting token
68
+ ```
69
+
70
+ `require_token()` with no scope accepts any usable token. For routes where a
71
+ token is one of several ways in, call `resolve_token(scope)` directly: it
72
+ returns `(api_token, None)` or `(None, (response, status))`.
73
+
74
+ ## Endpoints
75
+
76
+ | Method | Path | Auth |
77
+ |--------|---------------------------------|-------------------|
78
+ | POST | `/api/v1/auth/tokens` | email + password |
79
+ | GET | `/api/v1/auth/whoami` | Bearer token |
80
+ | DELETE | `/api/v1/auth/tokens/current` | Bearer token |
81
+
82
+ ```bash
83
+ curl -X POST https://<product>/api/v1/auth/tokens \
84
+ -H "Content-Type: application/json" \
85
+ -d '{"email": "dev@example.com", "password": "...", "name": "laptop"}'
86
+
87
+ curl https://<product>/api/v1/auth/whoami \
88
+ -H "Authorization: Bearer splent_..."
89
+ ```
90
+
91
+ Omitting `ttl_days` gives the product default; sending it as `null` gives a
92
+ token that never expires.
93
+
94
+ Failed credential attempts are throttled per caller and account. Users are
95
+ created inactive and an administrator activates them, so an account still
96
+ waiting is answered with `403` and a message that says exactly that, never
97
+ with "wrong password".
98
+
99
+ ## Pages
100
+
101
+ - `/developer/tokens` — the owner's tokens, by prefix, with a revoke button.
102
+ - `/developer/tokens/new` — create one; the plaintext is shown once.
103
+
104
+ A link appears in the authenticated sidebar
105
+ (`layout.authenticated_sidebar` hook).
106
+
107
+ ## Configuration
108
+
109
+ | Variable | Default | Meaning |
110
+ |--------------------------------|---------|----------------------------------|
111
+ | `APIKEYS_DEFAULT_TTL_DAYS` | 90 | Life of a token when none is asked |
112
+ | `APIKEYS_MAX_LOGIN_ATTEMPTS` | 5 | Failed attempts per window |
113
+ | `APIKEYS_LOGIN_WINDOW_SECONDS` | 300 | Length of that window |
114
+
115
+ All three have working defaults, so the feature needs no `.env` entry.
116
+
117
+ ## Install
118
+
119
+ ```bash
120
+ splent feature:install splent-io/splent_feature_apikeys
121
+ splent db:migrate splent_feature_apikeys
122
+ splent db:upgrade
123
+ ```
124
+
125
+ ## Tests
126
+
127
+ ```bash
128
+ splent feature:test splent_feature_apikeys
129
+ ```
@@ -0,0 +1,123 @@
1
+ # splent_feature_apikeys
2
+
3
+ Developer API tokens for a SPLENT product. A port of the UVLHub `apikeys`
4
+ feature to SPLENT conventions, and the only place in a product where a
5
+ password is exchanged for a credential a machine can carry.
6
+
7
+ It owns tokens and nothing else. It does not know what the tokens are used
8
+ for, and it never touches the `auth` feature: credentials are checked through
9
+ the `AuthenticationService` that auth already registers.
10
+
11
+ ## The model
12
+
13
+ `ApiToken` belongs to one user and keeps:
14
+
15
+ | Column | Meaning |
16
+ |----------------|------------------------------------------------------------|
17
+ | `user_id` | Owner, foreign key to auth's `user.id` |
18
+ | `name` | Human label chosen by the owner |
19
+ | `token_hash` | sha256 of the plaintext, hex. The plaintext is never stored |
20
+ | `prefix` | First 15 characters of the plaintext, indexed, safe to show |
21
+ | `scopes` | Comma separated, as in UVLHub |
22
+ | `created_at`, `expires_at`, `revoked_at`, `last_used_at` | lifecycle |
23
+
24
+ A plaintext token looks like `splent_<64 hex characters>`. The `splent_`
25
+ label makes a leaked token greppable; the prefix makes verification a single
26
+ indexed lookup instead of a table scan.
27
+
28
+ ## Scopes
29
+
30
+ `read_spl` and `write_spl`. Unknown names are dropped, and a request that
31
+ asks for nothing but unknown names is refused rather than silently producing
32
+ a token that can do nothing.
33
+
34
+ ## The service
35
+
36
+ `ApiTokenService`, registered under that name in the service locator.
37
+
38
+ ```python
39
+ api_token_service = service_proxy("ApiTokenService")
40
+
41
+ row, plaintext = api_token_service.mint(user, "laptop", ["write_spl"], ttl_days=90)
42
+ api_token_service.verify(plaintext) # row, or None if unknown/revoked/expired
43
+ api_token_service.find(plaintext) # row whatever its state, to explain a refusal
44
+ api_token_service.revoke(row.id, user) # only the owner can revoke
45
+ api_token_service.touch(row) # last_used_at, at most once every 5 minutes
46
+ api_token_service.authenticate(email, password) # (user, None) or (None, reason)
47
+ ```
48
+
49
+ `mint` returns the plaintext once. Nothing else in the product can ever
50
+ produce it again.
51
+
52
+ ## Guarding a route
53
+
54
+ ```python
55
+ from splent_io.splent_feature_apikeys.decorators import require_token
56
+
57
+ @some_bp.route("/api/v1/spls", methods=["POST"])
58
+ @require_token("write_spl")
59
+ def publish():
60
+ g.api_user # the owner
61
+ g.api_token # the presenting token
62
+ ```
63
+
64
+ `require_token()` with no scope accepts any usable token. For routes where a
65
+ token is one of several ways in, call `resolve_token(scope)` directly: it
66
+ returns `(api_token, None)` or `(None, (response, status))`.
67
+
68
+ ## Endpoints
69
+
70
+ | Method | Path | Auth |
71
+ |--------|---------------------------------|-------------------|
72
+ | POST | `/api/v1/auth/tokens` | email + password |
73
+ | GET | `/api/v1/auth/whoami` | Bearer token |
74
+ | DELETE | `/api/v1/auth/tokens/current` | Bearer token |
75
+
76
+ ```bash
77
+ curl -X POST https://<product>/api/v1/auth/tokens \
78
+ -H "Content-Type: application/json" \
79
+ -d '{"email": "dev@example.com", "password": "...", "name": "laptop"}'
80
+
81
+ curl https://<product>/api/v1/auth/whoami \
82
+ -H "Authorization: Bearer splent_..."
83
+ ```
84
+
85
+ Omitting `ttl_days` gives the product default; sending it as `null` gives a
86
+ token that never expires.
87
+
88
+ Failed credential attempts are throttled per caller and account. Users are
89
+ created inactive and an administrator activates them, so an account still
90
+ waiting is answered with `403` and a message that says exactly that, never
91
+ with "wrong password".
92
+
93
+ ## Pages
94
+
95
+ - `/developer/tokens` — the owner's tokens, by prefix, with a revoke button.
96
+ - `/developer/tokens/new` — create one; the plaintext is shown once.
97
+
98
+ A link appears in the authenticated sidebar
99
+ (`layout.authenticated_sidebar` hook).
100
+
101
+ ## Configuration
102
+
103
+ | Variable | Default | Meaning |
104
+ |--------------------------------|---------|----------------------------------|
105
+ | `APIKEYS_DEFAULT_TTL_DAYS` | 90 | Life of a token when none is asked |
106
+ | `APIKEYS_MAX_LOGIN_ATTEMPTS` | 5 | Failed attempts per window |
107
+ | `APIKEYS_LOGIN_WINDOW_SECONDS` | 300 | Length of that window |
108
+
109
+ All three have working defaults, so the feature needs no `.env` entry.
110
+
111
+ ## Install
112
+
113
+ ```bash
114
+ splent feature:install splent-io/splent_feature_apikeys
115
+ splent db:migrate splent_feature_apikeys
116
+ splent db:upgrade
117
+ ```
118
+
119
+ ## Tests
120
+
121
+ ```bash
122
+ splent feature:test splent_feature_apikeys
123
+ ```
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.3.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splent_feature_apikeys"
7
+ version = "0.1.1"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+
11
+ dependencies = [
12
+
13
+ ]
14
+
15
+ [tool.setuptools]
16
+ package-dir = { "" = "src" }
17
+ include-package-data = true
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+ exclude = ["*.tests", "*.tests.*"]
22
+
23
+ [tool.splent]
24
+ cli_version = "1.14.0"
25
+
26
+ # ── Feature Contract (auto-generated) ────────────────────────────────────────
27
+ # Do not edit manually — re-run `splent feature:contract --write` to refresh.
28
+ # Preserved across refreshes: description, category, tags, env, and
29
+ # requires.features_manual (dependencies invisible to static analysis).
30
+ [tool.splent.contract]
31
+ description = "Developer API tokens: mint, verify, revoke, and a Bearer decorator for the product's own API"
32
+ archetype = "full"
33
+ category = "security"
34
+ tags = ["api", "tokens", "authentication", "developer"]
35
+
36
+ [tool.splent.contract.provides]
37
+ routes = ["/api/v1/auth/tokens", "/api/v1/auth/tokens/current", "/api/v1/auth/whoami", "/developer/tokens", "/developer/tokens/<int:token_id>/revoke", "/developer/tokens/new"]
38
+ blueprints = []
39
+ models = ["ApiToken"]
40
+ commands = []
41
+ hooks = ["layout.authenticated_sidebar"]
42
+ services = ["ApiTokenService"]
43
+ signals = []
44
+ translations = []
45
+ docker = []
46
+
47
+ [tool.splent.contract.requires]
48
+ features = ["auth"]
49
+ env_vars = ["APIKEYS_DEFAULT_TTL_DAYS", "APIKEYS_LOGIN_WINDOW_SECONDS", "APIKEYS_MAX_ACCOUNT_ATTEMPTS", "APIKEYS_MAX_BODY_BYTES", "APIKEYS_MAX_LOGIN_ATTEMPTS", "APIKEYS_THROTTLE_MAX_KEYS"]
50
+ signals = []
51
+
52
+ [tool.splent.contract.extensible]
53
+ services = ["ApiTokenService"]
54
+ templates = ["apikeys/created.html", "apikeys/index.html", "apikeys/new.html", "hooks/sidebar_tokens.html"]
55
+ models = ["ApiToken"]
56
+ hooks = ["layout.authenticated_sidebar"]
57
+ routes = false
58
+
59
+ # ── Configuration ─────────────────────────────────────────────────────────
60
+ # What this feature reads from the environment, and what it falls back to.
61
+ [tool.splent.config]
62
+ # How long a key issued without an explicit expiry is good for.
63
+ APIKEYS_DEFAULT_TTL_DAYS = 90
64
+ # Rate limiting on the exchange endpoint, which takes a password. The window
65
+ # is per address, the account limit per account, so one address hammering
66
+ # many accounts and many addresses hammering one are both bounded.
67
+ APIKEYS_MAX_LOGIN_ATTEMPTS = 5
68
+ APIKEYS_LOGIN_WINDOW_SECONDS = 300
69
+ APIKEYS_MAX_ACCOUNT_ATTEMPTS = 20
70
+ # The throttle keeps its counters in memory. This caps how many it holds, so
71
+ # a flood of distinct addresses cannot grow it without bound.
72
+ APIKEYS_THROTTLE_MAX_KEYS = 10000
73
+ # Largest body accepted on the token endpoints. They take a short JSON
74
+ # object, and anything larger is not a client that means well.
75
+ APIKEYS_MAX_BODY_BYTES = 8192
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_apikeys
3
+ Version: 0.1.1
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
6
+
7
+ # splent_feature_apikeys
8
+
9
+ Developer API tokens for a SPLENT product. A port of the UVLHub `apikeys`
10
+ feature to SPLENT conventions, and the only place in a product where a
11
+ password is exchanged for a credential a machine can carry.
12
+
13
+ It owns tokens and nothing else. It does not know what the tokens are used
14
+ for, and it never touches the `auth` feature: credentials are checked through
15
+ the `AuthenticationService` that auth already registers.
16
+
17
+ ## The model
18
+
19
+ `ApiToken` belongs to one user and keeps:
20
+
21
+ | Column | Meaning |
22
+ |----------------|------------------------------------------------------------|
23
+ | `user_id` | Owner, foreign key to auth's `user.id` |
24
+ | `name` | Human label chosen by the owner |
25
+ | `token_hash` | sha256 of the plaintext, hex. The plaintext is never stored |
26
+ | `prefix` | First 15 characters of the plaintext, indexed, safe to show |
27
+ | `scopes` | Comma separated, as in UVLHub |
28
+ | `created_at`, `expires_at`, `revoked_at`, `last_used_at` | lifecycle |
29
+
30
+ A plaintext token looks like `splent_<64 hex characters>`. The `splent_`
31
+ label makes a leaked token greppable; the prefix makes verification a single
32
+ indexed lookup instead of a table scan.
33
+
34
+ ## Scopes
35
+
36
+ `read_spl` and `write_spl`. Unknown names are dropped, and a request that
37
+ asks for nothing but unknown names is refused rather than silently producing
38
+ a token that can do nothing.
39
+
40
+ ## The service
41
+
42
+ `ApiTokenService`, registered under that name in the service locator.
43
+
44
+ ```python
45
+ api_token_service = service_proxy("ApiTokenService")
46
+
47
+ row, plaintext = api_token_service.mint(user, "laptop", ["write_spl"], ttl_days=90)
48
+ api_token_service.verify(plaintext) # row, or None if unknown/revoked/expired
49
+ api_token_service.find(plaintext) # row whatever its state, to explain a refusal
50
+ api_token_service.revoke(row.id, user) # only the owner can revoke
51
+ api_token_service.touch(row) # last_used_at, at most once every 5 minutes
52
+ api_token_service.authenticate(email, password) # (user, None) or (None, reason)
53
+ ```
54
+
55
+ `mint` returns the plaintext once. Nothing else in the product can ever
56
+ produce it again.
57
+
58
+ ## Guarding a route
59
+
60
+ ```python
61
+ from splent_io.splent_feature_apikeys.decorators import require_token
62
+
63
+ @some_bp.route("/api/v1/spls", methods=["POST"])
64
+ @require_token("write_spl")
65
+ def publish():
66
+ g.api_user # the owner
67
+ g.api_token # the presenting token
68
+ ```
69
+
70
+ `require_token()` with no scope accepts any usable token. For routes where a
71
+ token is one of several ways in, call `resolve_token(scope)` directly: it
72
+ returns `(api_token, None)` or `(None, (response, status))`.
73
+
74
+ ## Endpoints
75
+
76
+ | Method | Path | Auth |
77
+ |--------|---------------------------------|-------------------|
78
+ | POST | `/api/v1/auth/tokens` | email + password |
79
+ | GET | `/api/v1/auth/whoami` | Bearer token |
80
+ | DELETE | `/api/v1/auth/tokens/current` | Bearer token |
81
+
82
+ ```bash
83
+ curl -X POST https://<product>/api/v1/auth/tokens \
84
+ -H "Content-Type: application/json" \
85
+ -d '{"email": "dev@example.com", "password": "...", "name": "laptop"}'
86
+
87
+ curl https://<product>/api/v1/auth/whoami \
88
+ -H "Authorization: Bearer splent_..."
89
+ ```
90
+
91
+ Omitting `ttl_days` gives the product default; sending it as `null` gives a
92
+ token that never expires.
93
+
94
+ Failed credential attempts are throttled per caller and account. Users are
95
+ created inactive and an administrator activates them, so an account still
96
+ waiting is answered with `403` and a message that says exactly that, never
97
+ with "wrong password".
98
+
99
+ ## Pages
100
+
101
+ - `/developer/tokens` — the owner's tokens, by prefix, with a revoke button.
102
+ - `/developer/tokens/new` — create one; the plaintext is shown once.
103
+
104
+ A link appears in the authenticated sidebar
105
+ (`layout.authenticated_sidebar` hook).
106
+
107
+ ## Configuration
108
+
109
+ | Variable | Default | Meaning |
110
+ |--------------------------------|---------|----------------------------------|
111
+ | `APIKEYS_DEFAULT_TTL_DAYS` | 90 | Life of a token when none is asked |
112
+ | `APIKEYS_MAX_LOGIN_ATTEMPTS` | 5 | Failed attempts per window |
113
+ | `APIKEYS_LOGIN_WINDOW_SECONDS` | 300 | Length of that window |
114
+
115
+ All three have working defaults, so the feature needs no `.env` entry.
116
+
117
+ ## Install
118
+
119
+ ```bash
120
+ splent feature:install splent-io/splent_feature_apikeys
121
+ splent db:migrate splent_feature_apikeys
122
+ splent db:upgrade
123
+ ```
124
+
125
+ ## Tests
126
+
127
+ ```bash
128
+ splent feature:test splent_feature_apikeys
129
+ ```
@@ -0,0 +1,27 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ src/splent_feature_apikeys.egg-info/PKG-INFO
5
+ src/splent_feature_apikeys.egg-info/SOURCES.txt
6
+ src/splent_feature_apikeys.egg-info/dependency_links.txt
7
+ src/splent_feature_apikeys.egg-info/top_level.txt
8
+ src/splent_io/splent_feature_apikeys/__init__.py
9
+ src/splent_io/splent_feature_apikeys/config.py
10
+ src/splent_io/splent_feature_apikeys/decorators.py
11
+ src/splent_io/splent_feature_apikeys/forms.py
12
+ src/splent_io/splent_feature_apikeys/hooks.py
13
+ src/splent_io/splent_feature_apikeys/models.py
14
+ src/splent_io/splent_feature_apikeys/repositories.py
15
+ src/splent_io/splent_feature_apikeys/routes.py
16
+ src/splent_io/splent_feature_apikeys/services.py
17
+ src/splent_io/splent_feature_apikeys/throttle.py
18
+ src/splent_io/splent_feature_apikeys/migrations/alembic.ini
19
+ src/splent_io/splent_feature_apikeys/migrations/env.py
20
+ src/splent_io/splent_feature_apikeys/migrations/script.py.mako
21
+ src/splent_io/splent_feature_apikeys/migrations/versions/089274640134_splent_feature_apikeys.py
22
+ src/splent_io/splent_feature_apikeys/templates/apikeys/created.html
23
+ src/splent_io/splent_feature_apikeys/templates/apikeys/index.html
24
+ src/splent_io/splent_feature_apikeys/templates/apikeys/new.html
25
+ src/splent_io/splent_feature_apikeys/templates/hooks/sidebar_tokens.html
26
+ src/splent_io/splent_feature_apikeys/translations/es/LC_MESSAGES/messages.mo
27
+ src/splent_io/splent_feature_apikeys/translations/es/LC_MESSAGES/messages.po
@@ -0,0 +1,35 @@
1
+ from splent_framework.blueprints.base_blueprint import create_blueprint
2
+ from splent_framework.services.service_locator import register_service
3
+
4
+ from splent_io.splent_feature_apikeys.services import ApiTokenService
5
+
6
+ apikeys_bp = create_blueprint(__name__)
7
+
8
+
9
+ def init_feature(app):
10
+ from splent_io.splent_feature_apikeys.throttle import configure_from_app
11
+
12
+ register_service(app, "ApiTokenService", ApiTokenService)
13
+ # config.inject_config has already run, so the throttle picks up the
14
+ # product's budget instead of the module defaults.
15
+ configure_from_app(app)
16
+ _exempt_token_api_from_csrf(app)
17
+
18
+
19
+ def _exempt_token_api_from_csrf(app):
20
+ """Machine API: trades email+password for a token, or revokes the bearer
21
+ token it is called with. Neither rides on the session cookie."""
22
+ csrf = app.extensions.get("splent_csrf")
23
+ if csrf is None:
24
+ return
25
+ from splent_io.splent_feature_apikeys.routes import (
26
+ api_create_token,
27
+ api_revoke_current_token,
28
+ )
29
+
30
+ csrf.exempt(api_create_token)
31
+ csrf.exempt(api_revoke_current_token)
32
+
33
+
34
+ def inject_context_vars(app):
35
+ return {}
@@ -0,0 +1,44 @@
1
+ """
2
+ apikeys feature configuration.
3
+
4
+ Injects environment variables into Flask app.config.
5
+ Every value has a working default, so the feature needs no .env entry to run.
6
+
7
+ To regenerate from source code: splent feature:inject-config splent_feature_apikeys
8
+ """
9
+
10
+ import os
11
+
12
+
13
+ def inject_config(app):
14
+ app.config.update(
15
+ {
16
+ # Life given to a token when the caller does not ask for one.
17
+ "APIKEYS_DEFAULT_TTL_DAYS": int(
18
+ os.getenv("APIKEYS_DEFAULT_TTL_DAYS", "90")
19
+ ),
20
+ # Failed credential attempts allowed per caller and window.
21
+ "APIKEYS_MAX_LOGIN_ATTEMPTS": int(
22
+ os.getenv("APIKEYS_MAX_LOGIN_ATTEMPTS", "5")
23
+ ),
24
+ "APIKEYS_LOGIN_WINDOW_SECONDS": int(
25
+ os.getenv("APIKEYS_LOGIN_WINDOW_SECONDS", "300")
26
+ ),
27
+ # Failed attempts allowed against ONE account from every address
28
+ # put together, for the distributed case a per address budget
29
+ # cannot see. Higher than the per address one on purpose, so the
30
+ # single attacker is caught by their own budget first.
31
+ "APIKEYS_MAX_ACCOUNT_ATTEMPTS": int(
32
+ os.getenv("APIKEYS_MAX_ACCOUNT_ATTEMPTS", "20")
33
+ ),
34
+ # Hard ceiling on how many throttle buckets one worker holds. The
35
+ # store is the only thing an unauthenticated caller can make this
36
+ # process allocate, so it is bounded rather than trusted.
37
+ "APIKEYS_THROTTLE_MAX_KEYS": int(
38
+ os.getenv("APIKEYS_THROTTLE_MAX_KEYS", "10000")
39
+ ),
40
+ # Largest body POST /api/v1/auth/tokens will read, in bytes. It
41
+ # carries an e-mail, a password and a label.
42
+ "APIKEYS_MAX_BODY_BYTES": int(os.getenv("APIKEYS_MAX_BODY_BYTES", "8192")),
43
+ }
44
+ )