charmlibs-interfaces-istio-request-auth 0.0.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.
- charmlibs_interfaces_istio_request_auth-0.0.1/.gitignore +184 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/CHANGELOG.md +11 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/PKG-INFO +30 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/README.md +11 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/pyproject.toml +74 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/src/charmlibs/interfaces/istio_request_auth/__init__.py +105 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py +221 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/src/charmlibs/interfaces/istio_request_auth/_version.py +15 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/src/charmlibs/interfaces/istio_request_auth/py.typed +0 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/tests/unit/conftest.py +15 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/tests/unit/test_istio_request_auth.py +269 -0
- charmlibs_interfaces_istio_request_auth-0.0.1/uv.lock +346 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
cos-tool*
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.coverage-*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
.report/
|
|
55
|
+
|
|
56
|
+
# Translations
|
|
57
|
+
*.mo
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
# Django stuff:
|
|
61
|
+
*.log
|
|
62
|
+
local_settings.py
|
|
63
|
+
db.sqlite3
|
|
64
|
+
db.sqlite3-journal
|
|
65
|
+
|
|
66
|
+
# Flask stuff:
|
|
67
|
+
instance/
|
|
68
|
+
.webassets-cache
|
|
69
|
+
|
|
70
|
+
# Scrapy stuff:
|
|
71
|
+
.scrapy
|
|
72
|
+
|
|
73
|
+
# Sphinx documentation
|
|
74
|
+
docs/_build/
|
|
75
|
+
|
|
76
|
+
# PyBuilder
|
|
77
|
+
.pybuilder/
|
|
78
|
+
target/
|
|
79
|
+
|
|
80
|
+
# Jupyter Notebook
|
|
81
|
+
.ipynb_checkpoints
|
|
82
|
+
|
|
83
|
+
# IPython
|
|
84
|
+
profile_default/
|
|
85
|
+
ipython_config.py
|
|
86
|
+
|
|
87
|
+
# pyenv
|
|
88
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
89
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
90
|
+
# .python-version
|
|
91
|
+
|
|
92
|
+
# pipenv
|
|
93
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
+
# install all needed dependencies.
|
|
97
|
+
#Pipfile.lock
|
|
98
|
+
|
|
99
|
+
# UV
|
|
100
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
101
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
102
|
+
# commonly ignored for libraries.
|
|
103
|
+
#uv.lock
|
|
104
|
+
|
|
105
|
+
# poetry
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
107
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
108
|
+
# commonly ignored for libraries.
|
|
109
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
110
|
+
#poetry.lock
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
#pdm.lock
|
|
115
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
116
|
+
# in version control.
|
|
117
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
118
|
+
.pdm.toml
|
|
119
|
+
.pdm-python
|
|
120
|
+
.pdm-build/
|
|
121
|
+
|
|
122
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
123
|
+
__pypackages__/
|
|
124
|
+
|
|
125
|
+
# Celery stuff
|
|
126
|
+
celerybeat-schedule
|
|
127
|
+
celerybeat.pid
|
|
128
|
+
|
|
129
|
+
# SageMath parsed files
|
|
130
|
+
*.sage.py
|
|
131
|
+
|
|
132
|
+
# Environments
|
|
133
|
+
.env
|
|
134
|
+
.venv
|
|
135
|
+
env/
|
|
136
|
+
venv/
|
|
137
|
+
ENV/
|
|
138
|
+
env.bak/
|
|
139
|
+
venv.bak/
|
|
140
|
+
|
|
141
|
+
# Spyder project settings
|
|
142
|
+
.spyderproject
|
|
143
|
+
.spyproject
|
|
144
|
+
|
|
145
|
+
# Rope project settings
|
|
146
|
+
.ropeproject
|
|
147
|
+
|
|
148
|
+
# mkdocs documentation
|
|
149
|
+
/site
|
|
150
|
+
|
|
151
|
+
# mypy
|
|
152
|
+
.mypy_cache/
|
|
153
|
+
.dmypy.json
|
|
154
|
+
dmypy.json
|
|
155
|
+
|
|
156
|
+
# Pyre type checker
|
|
157
|
+
.pyre/
|
|
158
|
+
|
|
159
|
+
# pytype static type analyzer
|
|
160
|
+
.pytype/
|
|
161
|
+
|
|
162
|
+
# Cython debug symbols
|
|
163
|
+
cython_debug/
|
|
164
|
+
|
|
165
|
+
# PyCharm
|
|
166
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
167
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
168
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
169
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
170
|
+
.idea/
|
|
171
|
+
|
|
172
|
+
# PyPI configuration file
|
|
173
|
+
.pypirc
|
|
174
|
+
|
|
175
|
+
# packed charms
|
|
176
|
+
.packed
|
|
177
|
+
|
|
178
|
+
# temporary directory for packing charms
|
|
179
|
+
.tmp
|
|
180
|
+
|
|
181
|
+
# uv.lock from example libraries as we don't commit these
|
|
182
|
+
.example/**/uv.lock
|
|
183
|
+
.tutorial/**/uv.lock
|
|
184
|
+
interfaces/.example/**/uv.lock
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## 0.0.1 - 22 April 2026
|
|
2
|
+
|
|
3
|
+
Initial release.
|
|
4
|
+
|
|
5
|
+
The initial `istio-request-auth` interface library provides the following features:
|
|
6
|
+
|
|
7
|
+
- Lets charms share their trusted JWT issuers and header-claim mappings as jwt_rules with the `istio-ingress-k8s` charms
|
|
8
|
+
- Lets charms specify multiple trusted issuers and their corresponding header-claim mappings as a list of jwt_rules
|
|
9
|
+
- Provides convienience data models for describing JWTRules
|
|
10
|
+
- Models the top level data bag as datamodel
|
|
11
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: charmlibs-interfaces-istio-request-auth
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The charmlibs.interfaces.istio_request_auth package.
|
|
5
|
+
Project-URL: Documentation, https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth
|
|
6
|
+
Project-URL: Repository, https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth
|
|
7
|
+
Project-URL: Issues, https://github.com/canonical/charmlibs/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/canonical/charmlibs/blob/main/interfaces/istio-request-auth/CHANGELOG.md
|
|
9
|
+
Author: Service Mesh
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: ops
|
|
17
|
+
Requires-Dist: pydantic>=2
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# charmlibs.interfaces.istio_request_auth
|
|
21
|
+
|
|
22
|
+
The `istio-request-auth` interface library.
|
|
23
|
+
|
|
24
|
+
To install, add `charmlibs-interfaces-istio-request-auth` to your Python dependencies. Then in your Python code, import as:
|
|
25
|
+
|
|
26
|
+
```py
|
|
27
|
+
from charmlibs.interfaces import istio_request_auth
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth) for more.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# charmlibs.interfaces.istio_request_auth
|
|
2
|
+
|
|
3
|
+
The `istio-request-auth` interface library.
|
|
4
|
+
|
|
5
|
+
To install, add `charmlibs-interfaces-istio-request-auth` to your Python dependencies. Then in your Python code, import as:
|
|
6
|
+
|
|
7
|
+
```py
|
|
8
|
+
from charmlibs.interfaces import istio_request_auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth) for more.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "charmlibs-interfaces-istio-request-auth"
|
|
3
|
+
description = "The charmlibs.interfaces.istio_request_auth package."
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
authors = [
|
|
7
|
+
{name="Service Mesh"},
|
|
8
|
+
]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"License :: OSI Approved :: Apache Software License",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: POSIX :: Linux",
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
]
|
|
16
|
+
dynamic = ["version"]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"ops",
|
|
19
|
+
"pydantic>=2",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
lint = [ # installed for `just lint interfaces/istio-request-auth` (unit, functional, and integration are also installed)
|
|
24
|
+
# "typing_extensions",
|
|
25
|
+
]
|
|
26
|
+
unit = [ # installed for `just unit interfaces/istio-request-auth`
|
|
27
|
+
"ops[testing]",
|
|
28
|
+
]
|
|
29
|
+
functional = [ # installed for `just functional interfaces/istio-request-auth`
|
|
30
|
+
]
|
|
31
|
+
integration = [ # installed for `just integration interfaces/istio-request-auth`
|
|
32
|
+
"jubilant",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth"
|
|
37
|
+
"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth"
|
|
38
|
+
"Issues" = "https://github.com/canonical/charmlibs/issues"
|
|
39
|
+
"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio-request-auth/CHANGELOG.md"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/charmlibs"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.version]
|
|
49
|
+
path = "src/charmlibs/interfaces/istio_request_auth/_version.py"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
extend = "../../pyproject.toml"
|
|
53
|
+
src = ["src", "tests/unit", "tests/functional", "tests/integration"] # correctly sort local imports in tests
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
56
|
+
# add additional per-file-ignores here to avoid overriding repo-level config
|
|
57
|
+
"tests/**/*" = [
|
|
58
|
+
# "E501", # line too long
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.pyright]
|
|
62
|
+
extends = "../../pyproject.toml"
|
|
63
|
+
include = ["src", "tests"]
|
|
64
|
+
pythonVersion = "3.10" # check no python > 3.10 features are used
|
|
65
|
+
|
|
66
|
+
[tool.charmlibs.functional]
|
|
67
|
+
ubuntu = [] # ubuntu versions to run functional tests with, e.g. "24.04" (defaults to just "latest")
|
|
68
|
+
pebble = [] # pebble versions to run functional tests with, e.g. "v1.0.0", "master" (defaults to no pebble versions)
|
|
69
|
+
sudo = false # whether to run functional tests with sudo (defaults to false)
|
|
70
|
+
|
|
71
|
+
[tool.charmlibs.integration]
|
|
72
|
+
# tags to run integration tests with (defaults to running once with no tag, i.e. tags = [''])
|
|
73
|
+
# Available in CI in tests/integration/pack.sh and integration tests as CHARMLIBS_TAG
|
|
74
|
+
tags = [] # Not used by the pack.sh and integration tests generated by the template
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Istio request authentication interface library.
|
|
16
|
+
|
|
17
|
+
This library provides the provider and requirer sides of the ``istio-request-auth``
|
|
18
|
+
relation interface for configuring Istio
|
|
19
|
+
`RequestAuthentication <https://istio.io/latest/docs/reference/config/security/request_authentication/>`_
|
|
20
|
+
resources via relation data (JWT rules, JWKS endpoints, claim-to-header mapping).
|
|
21
|
+
|
|
22
|
+
What is this library for?
|
|
23
|
+
=========================
|
|
24
|
+
|
|
25
|
+
The `istio-ingress-k8s <https://github.com/canonical/istio-ingress-k8s-operator/>`_ charm
|
|
26
|
+
wraps a `Kubernetes Gateway API <https://gateway-api.sigs.k8s.io/>`_ of class ``istio``. It
|
|
27
|
+
can connect to an OAuth 2.0 provider like the ``oauth2-proxy`` charm via the ``forward-auth``
|
|
28
|
+
relation to forward requests via an authentication stack for user authentication.
|
|
29
|
+
|
|
30
|
+
Istio also natively supports validating a pre-generated JWT against an issuer using a
|
|
31
|
+
``RequestAuthentication`` Kubernetes resource. This means a request containing a JWT in
|
|
32
|
+
the header can be natively authenticated by Istio instead of taking a detour via the
|
|
33
|
+
authentication stack, and the claims from the token are parsed and added as headers to
|
|
34
|
+
the downstream request. The `RequestAuthentication` resource is purely an Istio concept
|
|
35
|
+
offered by the CRDs native to Istio. Hence the interface is named with an `istio-` prefix.
|
|
36
|
+
|
|
37
|
+
For applications to take advantage of this feature, they need to tell Istio which issuers
|
|
38
|
+
they trust and which headers they want the claims in the token to be mapped to. To enable
|
|
39
|
+
this information exchange and add the appropriate ``RequestAuthentication`` resource, the
|
|
40
|
+
``istio-request-auth`` interface library is introduced.
|
|
41
|
+
|
|
42
|
+
Security note
|
|
43
|
+
=============
|
|
44
|
+
|
|
45
|
+
If a requirer is connected but has not provided valid (non-empty) ``jwt_rules``, no
|
|
46
|
+
``RequestAuthentication`` resource is created. This could allow unauthenticated
|
|
47
|
+
traffic. The provider exposes :meth:`~IstioRequestAuthProvider.get_connected_apps`
|
|
48
|
+
so the ingress charm can detect such applications and drop their ingress until valid
|
|
49
|
+
rules are provided.
|
|
50
|
+
|
|
51
|
+
Requirer usage::
|
|
52
|
+
|
|
53
|
+
from charmlibs.interfaces.istio_request_auth import (
|
|
54
|
+
ClaimToHeader,
|
|
55
|
+
JWTRule,
|
|
56
|
+
IstioRequestAuthRequirer,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
class MyAppCharm(CharmBase):
|
|
60
|
+
def __init__(self, *args):
|
|
61
|
+
super().__init__(*args)
|
|
62
|
+
self.request_auth = IstioRequestAuthRequirer(self)
|
|
63
|
+
|
|
64
|
+
def _publish_rules(self):
|
|
65
|
+
self.request_auth.publish_data([
|
|
66
|
+
JWTRule(
|
|
67
|
+
issuer="https://accounts.example.com",
|
|
68
|
+
claim_to_headers=[
|
|
69
|
+
ClaimToHeader(header="x-user-email", claim="email"),
|
|
70
|
+
],
|
|
71
|
+
),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
Provider usage::
|
|
75
|
+
|
|
76
|
+
from charmlibs.interfaces.istio_request_auth import IstioRequestAuthProvider
|
|
77
|
+
|
|
78
|
+
class MyIngressCharm(CharmBase):
|
|
79
|
+
def __init__(self, *args):
|
|
80
|
+
super().__init__(*args)
|
|
81
|
+
self.request_auth = IstioRequestAuthProvider(self)
|
|
82
|
+
|
|
83
|
+
def _reconcile(self):
|
|
84
|
+
valid = self.request_auth.get_data()
|
|
85
|
+
connected = self.request_auth.get_connected_apps()
|
|
86
|
+
invalid_apps = connected - set(valid.keys())
|
|
87
|
+
# Drop ingress for invalid_apps, create RequestAuthentication for valid
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
from ._istio_request_auth import (
|
|
91
|
+
ClaimToHeader,
|
|
92
|
+
FromHeader,
|
|
93
|
+
IstioRequestAuthProvider,
|
|
94
|
+
IstioRequestAuthRequirer,
|
|
95
|
+
JWTRule,
|
|
96
|
+
)
|
|
97
|
+
from ._version import __version__ as __version__
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
'ClaimToHeader',
|
|
101
|
+
'FromHeader',
|
|
102
|
+
'IstioRequestAuthProvider',
|
|
103
|
+
'IstioRequestAuthRequirer',
|
|
104
|
+
'JWTRule',
|
|
105
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Istio request authentication interface implementation.
|
|
16
|
+
|
|
17
|
+
Migrated from charmed-service-mesh-helpers interfaces/request_auth.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from ops.framework import Object
|
|
26
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from ops import CharmBase
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ClaimToHeader(BaseModel):
|
|
35
|
+
"""Maps a JWT claim to a request header."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(frozen=True)
|
|
38
|
+
|
|
39
|
+
header: str = Field(description='Target request header name')
|
|
40
|
+
claim: str = Field(description='JWT claim name to extract')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FromHeader(BaseModel):
|
|
44
|
+
"""Specifies a header location from which to extract a JWT."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(frozen=True)
|
|
47
|
+
|
|
48
|
+
name: str = Field(description='Header name')
|
|
49
|
+
prefix: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class JWTRule(BaseModel):
|
|
53
|
+
"""A single JWT validation rule provided by the requiring app."""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(frozen=True)
|
|
56
|
+
|
|
57
|
+
# The following fields mirror the JWTRule entry in the RequestAuthentication CRD.
|
|
58
|
+
# For details check https://istio.io/latest/docs/reference/config/security/request_authentication/#JWTRule
|
|
59
|
+
issuer: str = Field(description='Issuer URL for token validation')
|
|
60
|
+
jwks_uri: str | None = None
|
|
61
|
+
audiences: list[str] | None = None
|
|
62
|
+
forward_original_token: bool | None = None
|
|
63
|
+
# claim_to_headers allows mapping a single claim to multiple headers or
|
|
64
|
+
# multiple claims to the same header (concatenated with comma; missing
|
|
65
|
+
# claims are skipped).
|
|
66
|
+
claim_to_headers: list[ClaimToHeader] | None = None
|
|
67
|
+
# from_headers allows defining multiple potential header sources.
|
|
68
|
+
# The first one with a valid token will be used.
|
|
69
|
+
from_headers: list[FromHeader] | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _RequestAuthData(BaseModel):
|
|
73
|
+
"""Top-level databag model for the istio-request-auth relation.
|
|
74
|
+
|
|
75
|
+
Each field maps directly to a top-level key in the Juju application databag.
|
|
76
|
+
Use ``ops.Relation.load`` / ``ops.Relation.save`` to (de)serialise.
|
|
77
|
+
|
|
78
|
+
``jwt_rules`` defaults to ``None``. When it is missing, ``None``, or an
|
|
79
|
+
empty list the provider side treats the relation as not-ready and skips
|
|
80
|
+
the application.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
model_config = ConfigDict(frozen=True)
|
|
84
|
+
|
|
85
|
+
jwt_rules: list[JWTRule] | None = Field(
|
|
86
|
+
default=None,
|
|
87
|
+
description='List of JWT validation rules. Missing or empty means not ready.',
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class IstioRequestAuthProvider(Object):
|
|
92
|
+
"""Provider side of the istio-request-auth interface.
|
|
93
|
+
|
|
94
|
+
Used by the ingress charm to read JWT authentication rules from all related
|
|
95
|
+
applications.
|
|
96
|
+
|
|
97
|
+
Applications that are connected but have not provided valid (non-empty)
|
|
98
|
+
``jwt_rules`` are excluded from :meth:`get_data` but included in
|
|
99
|
+
:meth:`get_connected_apps`. Consumers can compare the two sets to
|
|
100
|
+
identify applications that have not yet provided data::
|
|
101
|
+
|
|
102
|
+
valid = provider.get_data()
|
|
103
|
+
connected = provider.get_connected_apps()
|
|
104
|
+
apps_without_data = connected - set(valid.keys())
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
charm: CharmBase,
|
|
110
|
+
relation_name: str = 'istio-request-auth',
|
|
111
|
+
):
|
|
112
|
+
"""Initialize the IstioRequestAuthProvider.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
charm: The charm that owns this provider.
|
|
116
|
+
relation_name: Name of the relation (default: "istio-request-auth").
|
|
117
|
+
"""
|
|
118
|
+
super().__init__(charm, relation_name)
|
|
119
|
+
self._charm = charm
|
|
120
|
+
self._relation_name = relation_name
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def is_ready(self) -> bool:
|
|
124
|
+
"""Check if any related application has provided valid request auth data.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if at least one requirer has published non-empty jwt_rules.
|
|
128
|
+
"""
|
|
129
|
+
return bool(self.get_data())
|
|
130
|
+
|
|
131
|
+
def get_connected_apps(self) -> set[str]:
|
|
132
|
+
"""Return the names of all applications connected over the relation.
|
|
133
|
+
|
|
134
|
+
This includes apps that have not yet provided valid data.
|
|
135
|
+
"""
|
|
136
|
+
apps: set[str] = set()
|
|
137
|
+
for relation in self._charm.model.relations.get(self._relation_name, []):
|
|
138
|
+
if relation.app:
|
|
139
|
+
apps.add(relation.app.name)
|
|
140
|
+
return apps
|
|
141
|
+
|
|
142
|
+
def get_data(self) -> dict[str, list[JWTRule]]:
|
|
143
|
+
"""Retrieve valid JWT rules from all related applications.
|
|
144
|
+
|
|
145
|
+
Uses ``ops.Relation.load`` to deserialise each application's databag
|
|
146
|
+
into :class:`RequestAuthData`. Only applications whose databag
|
|
147
|
+
contains a non-empty ``jwt_rules`` list are included.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A dict mapping application name to its list of ``JWTRule`` objects.
|
|
151
|
+
"""
|
|
152
|
+
result: dict[str, list[JWTRule]] = {}
|
|
153
|
+
relations = self._charm.model.relations.get(self._relation_name, [])
|
|
154
|
+
|
|
155
|
+
for relation in relations:
|
|
156
|
+
if not relation.app:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
app_name = relation.app.name
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
data = relation.load(_RequestAuthData, relation.app)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.exception(
|
|
165
|
+
'Failed to parse databag from application %s: %s',
|
|
166
|
+
app_name,
|
|
167
|
+
e,
|
|
168
|
+
)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if not data.jwt_rules:
|
|
172
|
+
logger.warning(
|
|
173
|
+
'Application %s has not provided jwt_rules',
|
|
174
|
+
app_name,
|
|
175
|
+
)
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
result[app_name] = data.jwt_rules
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class IstioRequestAuthRequirer(Object):
|
|
184
|
+
"""Requirer side of the istio-request-auth interface.
|
|
185
|
+
|
|
186
|
+
Used by downstream applications to publish their JWT authentication rules
|
|
187
|
+
to the ingress charm.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
charm: CharmBase,
|
|
193
|
+
relation_name: str = 'istio-request-auth',
|
|
194
|
+
):
|
|
195
|
+
"""Initialize the IstioRequestAuthRequirer.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
charm: The charm that owns this requirer.
|
|
199
|
+
relation_name: Name of the relation (default: "istio-request-auth").
|
|
200
|
+
"""
|
|
201
|
+
super().__init__(charm, relation_name)
|
|
202
|
+
self._charm = charm
|
|
203
|
+
self._relation_name = relation_name
|
|
204
|
+
|
|
205
|
+
def publish_data(self, jwt_rules: list[JWTRule]) -> None:
|
|
206
|
+
"""Publish JWT rules to the provider.
|
|
207
|
+
|
|
208
|
+
Uses ``ops.Relation.save`` to write a :class:`RequestAuthData` instance
|
|
209
|
+
to the application databag so ``jwt_rules`` appears as a top-level key.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
jwt_rules: The JWT validation rules to publish.
|
|
213
|
+
"""
|
|
214
|
+
if not self._charm.unit.is_leader():
|
|
215
|
+
logger.debug('Not leader, skipping request auth data publication')
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
data = _RequestAuthData(jwt_rules=jwt_rules)
|
|
219
|
+
|
|
220
|
+
for relation in self._charm.model.relations.get(self._relation_name, []):
|
|
221
|
+
relation.save(data, self._charm.app)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
__version__ = '0.0.1'
|
charmlibs_interfaces_istio_request_auth-0.0.1/src/charmlibs/interfaces/istio_request_auth/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Fixtures for unit tests, typically mocking out parts of the external system."""
|