soliplex 0.83__tar.gz → 0.83.2__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.
- soliplex-0.83.2/MANIFEST.in +2 -0
- {soliplex-0.83/src/soliplex.egg-info → soliplex-0.83.2}/PKG-INFO +1 -1
- {soliplex-0.83 → soliplex-0.83.2}/pyproject.toml +1 -1
- soliplex-0.83.2/src/soliplex/authn.py +155 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/authsystem.py +103 -23
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/installation.py +22 -6
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/loggers.py +23 -3
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/__init__.py +2 -1
- soliplex-0.83.2/src/soliplex/views/authn.py +535 -0
- soliplex-0.83.2/src/soliplex/views/templates/consent.html.mako +143 -0
- {soliplex-0.83 → soliplex-0.83.2/src/soliplex.egg-info}/PKG-INFO +1 -1
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex.egg-info/SOURCES.txt +2 -1
- soliplex-0.83/MANIFEST.in +0 -1
- soliplex-0.83/src/soliplex/authn.py +0 -82
- soliplex-0.83/src/soliplex/views/authn.py +0 -177
- {soliplex-0.83 → soliplex-0.83.2}/LICENSE +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/README.md +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/example/skills/bare-bones/SKILL.md +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/example/skills/stanzas/SKILL.md +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/setup.cfg +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/skills/soliplex-docs/SKILL.md +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/agents.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/agui/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/agui/parser.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/agui/persistence.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/agui/schema.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/agui/util.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/env.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/versions/216d48e1e2e5_soliplex_v0_53.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/versions/4b63e5f3f39e_soliplex_v0_51_1.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/versions/63edaa5987f6_soliplex_v0_67.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/versions/a1c7d3e90b42_soliplex_v0_80.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/versions/b7e2f41c9d05_soliplex_v0_82.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/alembic_migrations/versions/d5009d4f9874_soliplex_v0_44.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/authz/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/authz/persistence.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/authz/schema.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/capabilities/filesystem.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/capabilities/rag_audit.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/admin_users.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/ask.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/_common.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/admin_users.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/completions.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/databases.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/environment.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/installation.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/interpolation.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/logfire.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/logging.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/oidc.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/ollama.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/quizzes.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/room_authz.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/rooms.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/secrets.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/audit/skills.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/cli_util.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/database.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/main.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/ollama.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/room_authz.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/serve.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/cli/types.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/completions.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/_utils.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/agents.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/agui.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/completions.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/exceptions.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/interpolation.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/logfire.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/meta.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/middleware.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/quizzes.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/rag.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/rooms.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/routing.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/secrets.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/skills.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/config/tools.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/examples.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/installation.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/log_ingest.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/main.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/mcp_auth.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/mcp_client.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/mcp_server.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/models.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/ollama.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/quizzes.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/rag_audit.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/sandbox_audit.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/secrets.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/titles.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tools/__init__.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tools/agui_run_feedback.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tools/file_uploads.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tools/rag.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tui/cli.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tui/main.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tui/rest_api.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/tui/serve.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/util.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/agui.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/authz.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/completions.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/installation.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/log_ingest.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/quizzes.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/room_file_uploads.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/rooms.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/sandbox_workdirs.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/stats.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/streaming.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex/views/thread_file_uploads.py +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex.egg-info/dependency_links.txt +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex.egg-info/entry_points.txt +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex.egg-info/requires.txt +0 -0
- {soliplex-0.83 → soliplex-0.83.2}/src/soliplex.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soliplex
|
|
3
|
-
Version: 0.83
|
|
3
|
+
Version: 0.83.2
|
|
4
4
|
Summary: An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface.
|
|
5
5
|
Author-email: Enfold <info@enfoldsystems.net>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "soliplex"
|
|
7
|
-
version = "0.83"
|
|
7
|
+
version = "0.83.2"
|
|
8
8
|
description = "An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface."
|
|
9
9
|
authors = [{ name = "Enfold", email = "info@enfoldsystems.net" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Soliplex authentication support"""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import fastapi
|
|
6
|
+
import jwt
|
|
7
|
+
import starlette.config
|
|
8
|
+
from authlib.integrations import starlette_client
|
|
9
|
+
from fastapi import security
|
|
10
|
+
|
|
11
|
+
from soliplex import installation
|
|
12
|
+
from soliplex import loggers
|
|
13
|
+
from soliplex.config import authsystem as config_authsystem
|
|
14
|
+
|
|
15
|
+
UserClaims = dict[str, typing.Any]
|
|
16
|
+
|
|
17
|
+
oauth2_scheme = security.OAuth2PasswordBearer(
|
|
18
|
+
tokenUrl="token",
|
|
19
|
+
auto_error=False,
|
|
20
|
+
)
|
|
21
|
+
oauth2_predicate = fastapi.Depends(oauth2_scheme)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_oauth = None
|
|
25
|
+
|
|
26
|
+
JWT_VALIDATION_NO_TOKEN = "JWT validation failed (no token)"
|
|
27
|
+
JWT_VALIDATION_INVALID_TOKEN = "JWT validation failed (invalid token)"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_authsystem_config(
|
|
31
|
+
*,
|
|
32
|
+
the_installation: installation.Installation,
|
|
33
|
+
logger: loggers.LogWrapper,
|
|
34
|
+
system: str,
|
|
35
|
+
) -> config_authsystem.OIDCAuthSystemConfig:
|
|
36
|
+
"""Return the indicated authsystem config
|
|
37
|
+
|
|
38
|
+
Raise a 404 if the system is in no-auth mode, or if the named
|
|
39
|
+
authsystem config is not found.
|
|
40
|
+
"""
|
|
41
|
+
msg = loggers.AUTHN_NO_AUTH_MODE
|
|
42
|
+
|
|
43
|
+
for config in the_installation.oidc_auth_system_configs:
|
|
44
|
+
msg = loggers.AUTHN_UNKNOWN_AUTHSYSTEM
|
|
45
|
+
|
|
46
|
+
if config.id == system:
|
|
47
|
+
return config
|
|
48
|
+
|
|
49
|
+
logger.error(msg)
|
|
50
|
+
raise fastapi.HTTPException(
|
|
51
|
+
status_code=404,
|
|
52
|
+
detail=msg,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_oauth(
|
|
57
|
+
the_installation: installation.Installation,
|
|
58
|
+
) -> starlette_client.OAuth:
|
|
59
|
+
global _oauth
|
|
60
|
+
|
|
61
|
+
if _oauth is None:
|
|
62
|
+
config_data = {} # Or use .env
|
|
63
|
+
config = starlette.config.Config(environ=config_data)
|
|
64
|
+
_oauth = starlette_client.OAuth(config)
|
|
65
|
+
|
|
66
|
+
for auth_system in the_installation.oidc_auth_system_configs:
|
|
67
|
+
auth_system_kwargs = auth_system.oauth_client_kwargs
|
|
68
|
+
_oauth.register(**auth_system_kwargs)
|
|
69
|
+
|
|
70
|
+
return _oauth
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def authenticate(
|
|
74
|
+
the_installation: installation.Installation,
|
|
75
|
+
logger: loggers.LogWrapper,
|
|
76
|
+
token: str,
|
|
77
|
+
) -> UserClaims:
|
|
78
|
+
# See #316
|
|
79
|
+
if the_installation.auth_disabled:
|
|
80
|
+
return installation.NO_AUTH_MODE_USER_TOKEN
|
|
81
|
+
|
|
82
|
+
if token is None:
|
|
83
|
+
logger.info(loggers.AUTHN_JWT_NOT_SET)
|
|
84
|
+
raise fastapi.HTTPException(
|
|
85
|
+
status_code=401,
|
|
86
|
+
detail=JWT_VALIDATION_NO_TOKEN,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
errors = {} # accumulated; only reported if all systems miss
|
|
90
|
+
|
|
91
|
+
for auth_system in the_installation.oidc_auth_system_configs:
|
|
92
|
+
payload, msg = validate_access_token(
|
|
93
|
+
token,
|
|
94
|
+
auth_system,
|
|
95
|
+
)
|
|
96
|
+
if payload is not None:
|
|
97
|
+
return payload
|
|
98
|
+
|
|
99
|
+
errors[auth_system.id] = msg
|
|
100
|
+
|
|
101
|
+
for oidc_system_id, msg in errors.items():
|
|
102
|
+
logger.error(
|
|
103
|
+
loggers.AUTHN_JWT_INVALID,
|
|
104
|
+
oidc_system_id=oidc_system_id,
|
|
105
|
+
reason=msg,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
raise fastapi.HTTPException(
|
|
109
|
+
status_code=401,
|
|
110
|
+
detail=JWT_VALIDATION_INVALID_TOKEN,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_access_token(
|
|
115
|
+
token: str,
|
|
116
|
+
auth_system: config_authsystem.OIDCAuthSystemConfig,
|
|
117
|
+
) -> tuple[dict | None, str | None, int | None]:
|
|
118
|
+
"""Return '(payload, None)' if successful
|
|
119
|
+
|
|
120
|
+
Return '(None, msg)' if failed.
|
|
121
|
+
"""
|
|
122
|
+
payload = msg = None
|
|
123
|
+
try:
|
|
124
|
+
payload = jwt.decode(
|
|
125
|
+
token,
|
|
126
|
+
auth_system.token_validation_pem,
|
|
127
|
+
algorithms=["RS256"],
|
|
128
|
+
issuer=auth_system.issuer,
|
|
129
|
+
options={
|
|
130
|
+
"verify_aud": False,
|
|
131
|
+
"require": ["exp", "iat", "iss", "sub"],
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
except jwt.InvalidIssuerError:
|
|
135
|
+
msg = loggers.AUTHN_JWT_PARSE_INVALID_ISS
|
|
136
|
+
except jwt.InvalidSignatureError:
|
|
137
|
+
msg = loggers.AUTHN_JWT_PARSE_INVALID_SIG
|
|
138
|
+
except jwt.MissingRequiredClaimError as exc:
|
|
139
|
+
msg = f"{loggers.AUTHN_JWT_PARSE_REQUIRED_CLAIM}:{exc.claim}"
|
|
140
|
+
except jwt.ExpiredSignatureError:
|
|
141
|
+
msg = loggers.AUTHN_JWT_PARSE_EXPIRED_SIG
|
|
142
|
+
except jwt.InvalidTokenError:
|
|
143
|
+
msg = loggers.AUTHN_JWT_PARSE_OTHER
|
|
144
|
+
|
|
145
|
+
if msg is not None:
|
|
146
|
+
return None, msg
|
|
147
|
+
|
|
148
|
+
if payload.get("azp") not in auth_system.accepted_azp_list:
|
|
149
|
+
return None, loggers.AUTHN_JWT_BIND_AZP
|
|
150
|
+
|
|
151
|
+
for claim, expected in auth_system.required_claims.items():
|
|
152
|
+
if payload.get(claim) != expected:
|
|
153
|
+
return None, f"{loggers.AUTHN_JWT_BIND_CLAIM}:{claim}"
|
|
154
|
+
|
|
155
|
+
return payload, None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations # forward refs in typing decls
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
+
import enum
|
|
4
5
|
import pathlib
|
|
5
6
|
import ssl
|
|
6
7
|
import typing
|
|
@@ -12,6 +13,7 @@ from . import interpolation as config_interp
|
|
|
12
13
|
if typing.TYPE_CHECKING: # avoid an import cycle at runtime
|
|
13
14
|
from . import installation as config_installation
|
|
14
15
|
|
|
16
|
+
_default_list_field = _utils._default_list_field
|
|
15
17
|
_no_repr_no_compare_none = _utils._no_repr_no_compare_none
|
|
16
18
|
_secret_whole_or_literal_field = config_interp.secret_whole_or_literal_field
|
|
17
19
|
|
|
@@ -23,6 +25,11 @@ _secret_whole_or_literal_field = config_interp.secret_whole_or_literal_field
|
|
|
23
25
|
WELL_KNOWN_OPENID_CONFIGURATION = ".well-known/openid-configuration"
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
class UnlistedFrontendOriginPolicy(enum.StrEnum):
|
|
29
|
+
CONSENT_REQUIRED = "consent-required"
|
|
30
|
+
DENY_ALL = "deny-all"
|
|
31
|
+
|
|
32
|
+
|
|
26
33
|
@dataclasses.dataclass(kw_only=True)
|
|
27
34
|
class OIDCAuthSystemConfig:
|
|
28
35
|
id: str
|
|
@@ -36,6 +43,65 @@ class OIDCAuthSystemConfig:
|
|
|
36
43
|
# literal. 'env:' markers are not honored here.
|
|
37
44
|
client_secret: str = _secret_whole_or_literal_field(default="")
|
|
38
45
|
oidc_client_pem_path: pathlib.Path = None
|
|
46
|
+
consent_template_path: pathlib.Path = None
|
|
47
|
+
|
|
48
|
+
allowed_frontend_origins: list[str] = _default_list_field()
|
|
49
|
+
unlisted_frontend_origin: UnlistedFrontendOriginPolicy = (
|
|
50
|
+
UnlistedFrontendOriginPolicy.CONSENT_REQUIRED
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_accepted_azp_list: list[str] | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def accepted_azp_list(self) -> list[str]:
|
|
57
|
+
"""Accepted values for the 'azp' token claim."""
|
|
58
|
+
if self._accepted_azp_list is not None:
|
|
59
|
+
return self._accepted_azp_list
|
|
60
|
+
else:
|
|
61
|
+
return [self.client_id]
|
|
62
|
+
|
|
63
|
+
_issuer: str | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def issuer(self) -> str:
|
|
67
|
+
"""Expected value for the 'iss' token claim."""
|
|
68
|
+
if self._issuer is not None:
|
|
69
|
+
return self._issuer
|
|
70
|
+
else:
|
|
71
|
+
return self.server_url
|
|
72
|
+
|
|
73
|
+
# Required claims: must match values
|
|
74
|
+
required_claims: dict[str, str] = _utils._default_dict_field()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def server_metadata_url(self):
|
|
78
|
+
"""URL for discovery of OAuth metadata"""
|
|
79
|
+
return f"{self.server_url}/{WELL_KNOWN_OPENID_CONFIGURATION}"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def oauth_client_kwargs(self) -> dict:
|
|
83
|
+
"""Registered kwargs for this authsystem's 'ouath' client factory"""
|
|
84
|
+
client_kwargs = {}
|
|
85
|
+
|
|
86
|
+
if self.scope is not None:
|
|
87
|
+
client_kwargs["scope"] = self.scope
|
|
88
|
+
|
|
89
|
+
if self.oidc_client_pem_path is not None:
|
|
90
|
+
client_kwargs["verify"] = ssl.create_default_context(
|
|
91
|
+
cafile=self.oidc_client_pem_path
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
client_secret = config_interp.resolve_field(self, "client_secret")
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"name": self.id,
|
|
98
|
+
"server_metadata_url": self.server_metadata_url,
|
|
99
|
+
"client_id": self.client_id,
|
|
100
|
+
"client_secret": client_secret,
|
|
101
|
+
"client_kwargs": client_kwargs,
|
|
102
|
+
# added by the auth setup
|
|
103
|
+
# "authorize_state": main.SESSION_SECRET_KEY,
|
|
104
|
+
}
|
|
39
105
|
|
|
40
106
|
# Set in 'from_yaml' below
|
|
41
107
|
_installation_config: config_installation.InstallationConfig = (
|
|
@@ -59,6 +125,26 @@ class OIDCAuthSystemConfig:
|
|
|
59
125
|
config_path.parent / oidc_client_pem_path
|
|
60
126
|
)
|
|
61
127
|
|
|
128
|
+
consent_template_path = config_dict.pop("consent_template_path", None)
|
|
129
|
+
if consent_template_path is not None:
|
|
130
|
+
config_dict["consent_template_path"] = (
|
|
131
|
+
config_path.parent / consent_template_path
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
ufo = config_dict.pop("unlisted_frontend_origin", None)
|
|
135
|
+
if ufo is not None:
|
|
136
|
+
config_dict["unlisted_frontend_origin"] = (
|
|
137
|
+
UnlistedFrontendOriginPolicy(ufo)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
aal = config_dict.pop("accepted_azp_list", None)
|
|
141
|
+
if aal is not None:
|
|
142
|
+
config_dict["_accepted_azp_list"] = aal
|
|
143
|
+
|
|
144
|
+
iss = config_dict.pop("issuer", None)
|
|
145
|
+
if iss is not None:
|
|
146
|
+
config_dict["_issuer"] = iss
|
|
147
|
+
|
|
62
148
|
try:
|
|
63
149
|
return cls(**config_dict)
|
|
64
150
|
except Exception as exc:
|
|
@@ -89,32 +175,26 @@ class OIDCAuthSystemConfig:
|
|
|
89
175
|
# Absolutized against the config dir on load. See #1228.
|
|
90
176
|
result["oidc_client_pem_path"] = str(self.oidc_client_pem_path)
|
|
91
177
|
|
|
92
|
-
|
|
178
|
+
if self.consent_template_path is not None:
|
|
179
|
+
# Absolutized against the config dir on load. See #1228.
|
|
180
|
+
result["consent_template_path"] = str(self.consent_template_path)
|
|
93
181
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return f"{self.server_url}/{WELL_KNOWN_OPENID_CONFIGURATION}"
|
|
182
|
+
if self.allowed_frontend_origins:
|
|
183
|
+
result["allowed_frontend_origins"] = self.allowed_frontend_origins
|
|
97
184
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
185
|
+
if (
|
|
186
|
+
self.unlisted_frontend_origin
|
|
187
|
+
is not UnlistedFrontendOriginPolicy.CONSENT_REQUIRED
|
|
188
|
+
):
|
|
189
|
+
result["unlisted_frontend_origin"] = self.unlisted_frontend_origin
|
|
101
190
|
|
|
102
|
-
if self.
|
|
103
|
-
|
|
191
|
+
if self._accepted_azp_list is not None:
|
|
192
|
+
result["accepted_azp_list"] = self._accepted_azp_list
|
|
104
193
|
|
|
105
|
-
if self.
|
|
106
|
-
|
|
107
|
-
cafile=self.oidc_client_pem_path
|
|
108
|
-
)
|
|
194
|
+
if self._issuer is not None:
|
|
195
|
+
result["issuer"] = self._issuer
|
|
109
196
|
|
|
110
|
-
|
|
197
|
+
if self.required_claims:
|
|
198
|
+
result["required_claims"] = self.required_claims
|
|
111
199
|
|
|
112
|
-
return
|
|
113
|
-
"name": self.id,
|
|
114
|
-
"server_metadata_url": self.server_metadata_url,
|
|
115
|
-
"client_id": self.client_id,
|
|
116
|
-
"client_secret": client_secret,
|
|
117
|
-
"client_kwargs": client_kwargs,
|
|
118
|
-
# added by the auth setup
|
|
119
|
-
# "authorize_state": main.SESSION_SECRET_KEY,
|
|
120
|
-
}
|
|
200
|
+
return result
|
|
@@ -1356,19 +1356,35 @@ class InstallationConfig:
|
|
|
1356
1356
|
oidc_config = oidc_path / "config.yaml"
|
|
1357
1357
|
config_yaml = _load_config_yaml(oidc_config)
|
|
1358
1358
|
|
|
1359
|
+
top_level_defaults = {}
|
|
1360
|
+
|
|
1359
1361
|
oidc_client_pem_path = config_yaml.get("oidc_client_pem_path")
|
|
1362
|
+
|
|
1360
1363
|
if oidc_client_pem_path is not None:
|
|
1361
|
-
oidc_client_pem_path =
|
|
1364
|
+
top_level_defaults["oidc_client_pem_path"] = (
|
|
1365
|
+
oidc_path / oidc_client_pem_path
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
consent_template_path = config_yaml.get("consent_template_path")
|
|
1369
|
+
|
|
1370
|
+
if consent_template_path is not None:
|
|
1371
|
+
top_level_defaults["consent_template_path"] = (
|
|
1372
|
+
oidc_path / consent_template_path
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
afo = config_yaml.get("allowed_frontend_origins")
|
|
1376
|
+
if afo is not None:
|
|
1377
|
+
top_level_defaults["allowed_frontend_origins"] = afo
|
|
1378
|
+
|
|
1379
|
+
ufo = config_yaml.get("unlisted_frontend_origin")
|
|
1380
|
+
if ufo is not None:
|
|
1381
|
+
top_level_defaults["unlisted_frontend_origin"] = ufo
|
|
1362
1382
|
|
|
1363
1383
|
for auth_system_yaml in config_yaml["auth_systems"]:
|
|
1364
|
-
if "oidc_client_pem_path" not in auth_system_yaml:
|
|
1365
|
-
auth_system_yaml["oidc_client_pem_path"] = (
|
|
1366
|
-
oidc_client_pem_path
|
|
1367
|
-
)
|
|
1368
1384
|
oas_config = config_authsystem.OIDCAuthSystemConfig.from_yaml(
|
|
1369
1385
|
self,
|
|
1370
1386
|
oidc_config,
|
|
1371
|
-
auth_system_yaml,
|
|
1387
|
+
top_level_defaults | auth_system_yaml,
|
|
1372
1388
|
)
|
|
1373
1389
|
oas_configs.append(oas_config)
|
|
1374
1390
|
|
|
@@ -45,11 +45,31 @@ UPLOADS_POST_ROOM_THREAD = "uploads post room thread"
|
|
|
45
45
|
WORKDIRS_GET_ROOM_THREAD_RUN = "workdirs get room thread run"
|
|
46
46
|
WORKDIRS_GET_ROOM_THREAD_RUN_FILE = "workdirs get room thread run file"
|
|
47
47
|
|
|
48
|
+
AUTHN_BAD_REQUEST = "authn bad request"
|
|
49
|
+
AUTHN_CONSENT_CANCELLED = "unlisted frontend origin: sign-in cancelled"
|
|
50
|
+
AUTHN_CONSENT_CONFIRMED = "unlisted frontend origin: sign-in confirmed"
|
|
51
|
+
AUTHN_CONSENT_INVALID = "unlisted frontend origin: invalid confirmation"
|
|
52
|
+
AUTHN_CONSENT_REQUESTED = "unlisted frontend origin: confirmation requested"
|
|
53
|
+
AUTHN_CONSENT_TEMPLATE_FAILED = "consent template failed to render"
|
|
54
|
+
AUTHN_NO_RETURN_TO = "no return_to stored in session"
|
|
55
|
+
AUTHN_UNLISTED_ORIGIN_DENIED = "unlisted frontend origin denied"
|
|
48
56
|
AUTHN_LOGGER_NAME = "soliplex.authn"
|
|
49
|
-
|
|
50
|
-
AUTHN_JWT_INVALID = "JWT validation failed"
|
|
51
|
-
AUTHN_JWT_VALID = "JWT validation succeeded"
|
|
57
|
+
AUTHN_UNKNOWN_AUTHSYSTEM = "unknown auth system"
|
|
52
58
|
AUTHN_NO_AUTH_MODE = "system in no-auth mode"
|
|
59
|
+
|
|
60
|
+
AUTHN_JWT_NOT_SET = "JWT not set"
|
|
61
|
+
AUTHN_JWT_VALID = "JWT validation succeeded"
|
|
62
|
+
AUTHN_JWT_INVALID = "JWT validation failed"
|
|
63
|
+
|
|
64
|
+
AUTHN_JWT_PARSE_INVALID_ISS = "jwt-parse-inv-iss"
|
|
65
|
+
AUTHN_JWT_PARSE_INVALID_SIG = "jwt-parse-inv-sig"
|
|
66
|
+
AUTHN_JWT_PARSE_REQUIRED_CLAIM = "jwt-parse-req-claim"
|
|
67
|
+
AUTHN_JWT_PARSE_EXPIRED_SIG = "jwt-parse-exp-sig"
|
|
68
|
+
AUTHN_JWT_PARSE_OTHER = "jwt-parse-other"
|
|
69
|
+
|
|
70
|
+
AUTHN_JWT_BIND_AZP = "jwt-bind-azp"
|
|
71
|
+
AUTHN_JWT_BIND_CLAIM = "jwt-bind-claim"
|
|
72
|
+
|
|
53
73
|
AUTHN_GET_LOGIN = "get login"
|
|
54
74
|
AUTHN_GET_LOGIN_SYSTEM = "get login system"
|
|
55
75
|
AUTHN_GET_AUTH_SYSTEM = "get auth system"
|
|
@@ -49,10 +49,11 @@ async def get_the_user_claims(
|
|
|
49
49
|
try:
|
|
50
50
|
return authn_module.authenticate(
|
|
51
51
|
the_installation=the_installation,
|
|
52
|
+
logger=the_unauth_logger,
|
|
52
53
|
token=token,
|
|
53
54
|
)
|
|
54
55
|
except fastapi.HTTPException:
|
|
55
|
-
the_unauth_logger.
|
|
56
|
+
the_unauth_logger.error( # noqa: TRY400 `authenticate' logged details
|
|
56
57
|
loggers.AUTHN_GET_USER_CLAIMS_FAILED,
|
|
57
58
|
)
|
|
58
59
|
raise
|