This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-atr-experiments.git
The following commit(s) were added to refs/heads/main by this push:
new f92ba0e Add blueprint code from branch by @netomi
f92ba0e is described below
commit f92ba0e20c8982e67f6f581bcf30c172feed665a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Feb 17 19:54:58 2025 +0200
Add blueprint code from branch by @netomi
---
atr/blueprints/__init__.py | 32 +++++++
atr/blueprints/secret/__init__.py | 49 ++++++++++
atr/blueprints/secret/secret.py | 160 +++++++++++++++++++++++++++++++
atr/routes.py | 150 -----------------------------
atr/server.py | 4 +
atr/static/css/atr.css | 2 +-
atr/templates/add-release-candidate.html | 2 +-
atr/templates/data-browser.html | 2 +-
atr/templates/pages.html | 42 +-------
atr/templates/user-keys-add.html | 2 +-
typestubs/asfquart/auth.pyi | 87 +++++++++--------
11 files changed, 297 insertions(+), 235 deletions(-)
diff --git a/atr/blueprints/__init__.py b/atr/blueprints/__init__.py
new file mode 100644
index 0000000..d5ccb09
--- /dev/null
+++ b/atr/blueprints/__init__.py
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from importlib import import_module
+from importlib.util import find_spec
+
+from asfquart.base import QuartApp
+
+_BLUEPRINT_MODULES = ["secret"]
+
+
+def register_blueprints(app: QuartApp) -> None:
+ for routes_name in _BLUEPRINT_MODULES:
+ routes_fqn = f"atr.blueprints.{routes_name}.{routes_name}"
+ spec = find_spec(routes_fqn)
+ if spec is not None:
+ module = import_module(routes_fqn)
+ app.register_blueprint(module.blueprint)
diff --git a/atr/blueprints/secret/__init__.py
b/atr/blueprints/secret/__init__.py
new file mode 100644
index 0000000..df80c2d
--- /dev/null
+++ b/atr/blueprints/secret/__init__.py
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from quart import Blueprint
+
+from asfquart.auth import Requirements as R
+from asfquart.auth import require
+from asfquart.base import ASFQuartException
+from asfquart.session import read as session_read
+
+blueprint = Blueprint("secret_blueprint", __name__, url_prefix="/secret")
+
+ALLOWED_USERS = {
+ "cwells",
+ "fluxo",
+ "gmcdonald",
+ "humbedooh",
+ "sbp",
+ "tn",
+ "wave",
+}
+
+
[email protected]_request
+async def before_request_func() -> None:
+ @require(R.committer)
+ async def check_logged_in() -> None:
+ session = await session_read()
+ if session is None:
+ raise ASFQuartException("Not authenticated", errorcode=401)
+
+ if session.uid not in ALLOWED_USERS:
+ raise ASFQuartException("You are not authorized to access the
admin interface", errorcode=403)
+
+ await check_logged_in()
diff --git a/atr/blueprints/secret/secret.py b/atr/blueprints/secret/secret.py
new file mode 100644
index 0000000..d0daa5b
--- /dev/null
+++ b/atr/blueprints/secret/secret.py
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+
+import httpx
+from quart import current_app, render_template, request
+from sqlmodel import select
+
+from asfquart.base import ASFQuartException
+
+from ...models import (
+ PMC,
+ DistributionChannel,
+ Package,
+ PMCKeyLink,
+ ProductLine,
+ PublicSigningKey,
+ Release,
+ VotePolicy,
+)
+from . import blueprint
+
+
[email protected]("/data")
[email protected]("/data/<model>")
+async def secret_data(model: str = "PMC") -> str:
+ "Browse all records in the database."
+
+ # Map of model names to their classes
+ models = {
+ "PMC": PMC,
+ "Release": Release,
+ "Package": Package,
+ "VotePolicy": VotePolicy,
+ "ProductLine": ProductLine,
+ "DistributionChannel": DistributionChannel,
+ "PublicSigningKey": PublicSigningKey,
+ "PMCKeyLink": PMCKeyLink,
+ }
+
+ if model not in models:
+ # Default to PMC if invalid model specified
+ model = "PMC"
+
+ async_session = current_app.config["async_session"]
+ async with async_session() as db_session:
+ # Get all records for the selected model
+ statement = select(models[model])
+ records = (await db_session.execute(statement)).scalars().all()
+
+ # Convert records to dictionaries for JSON serialization
+ records_dict = []
+ for record in records:
+ if hasattr(record, "dict"):
+ record_dict = record.dict()
+ else:
+ # Fallback for models without dict() method
+ record_dict = {
+ "id": getattr(record, "id", None),
+ "storage_key": getattr(record, "storage_key", None),
+ }
+ for key in record.__dict__:
+ if not key.startswith("_"):
+ record_dict[key] = getattr(record, key)
+ records_dict.append(record_dict)
+
+ return await render_template("data-browser.html",
models=list(models.keys()), model=model, records=records_dict)
+
+
[email protected]("/pmcs/update", methods=["GET", "POST"])
+async def secret_pmcs_update() -> str:
+ "Update PMCs from remote, authoritative committee-info.json."
+
+ if request.method == "POST":
+ # Fetch committee-info.json from Whimsy
+ WHIMSY_URL = "https://whimsy.apache.org/public/committee-info.json"
+ async with httpx.AsyncClient() as client:
+ try:
+ response = await client.get(WHIMSY_URL)
+ response.raise_for_status()
+ data = response.json()
+ except (httpx.RequestError, json.JSONDecodeError) as e:
+ raise ASFQuartException(f"Failed to fetch committee data:
{str(e)}", errorcode=500)
+
+ committees = data.get("committees", {})
+ updated_count = 0
+
+ async_session = current_app.config["async_session"]
+ async with async_session() as db_session:
+ async with db_session.begin():
+ for committee_id, info in committees.items():
+ # Skip non-PMC committees
+ if not info.get("pmc", False):
+ continue
+
+ # Get or create PMC
+ statement = select(PMC).where(PMC.project_name ==
committee_id)
+ pmc = (await
db_session.execute(statement)).scalar_one_or_none()
+ if not pmc:
+ pmc = PMC(project_name=committee_id)
+ db_session.add(pmc)
+
+ # Update PMC data
+ roster = info.get("roster", {})
+ # TODO: Here we say that roster == pmc_members ==
committers
+ # We ought to do this more accurately instead
+ pmc.pmc_members = list(roster.keys())
+ pmc.committers = list(roster.keys())
+
+ # Mark chairs as release managers
+ # TODO: Who else is a release manager? How do we know?
+ chairs = [m["id"] for m in info.get("chairs", [])]
+ pmc.release_managers = chairs
+
+ updated_count += 1
+
+ # Add special entry for Tooling PMC
+ # Not clear why, but it's not in the Whimsy data
+ statement = select(PMC).where(PMC.project_name == "tooling")
+ tooling_pmc = (await
db_session.execute(statement)).scalar_one_or_none()
+ if not tooling_pmc:
+ tooling_pmc = PMC(project_name="tooling")
+ db_session.add(tooling_pmc)
+ updated_count += 1
+
+ # Update Tooling PMC data
+ # Could put this in the "if not tooling_pmc" block, perhaps
+ tooling_pmc.pmc_members = ["wave", "tn", "sbp"]
+ tooling_pmc.committers = ["wave", "tn", "sbp"]
+ tooling_pmc.release_managers = ["wave"]
+
+ return f"Successfully updated {updated_count} PMCs from Whimsy"
+
+ # For GET requests, show the update form
+ return await render_template("update-pmcs.html")
+
+
[email protected]("/debug/database")
+async def secret_debug_database() -> str:
+ """Debug information about the database."""
+ async_session = current_app.config["async_session"]
+ async with async_session() as db_session:
+ statement = select(PMC)
+ pmcs = (await db_session.execute(statement)).scalars().all()
+ return f"Database using {current_app.config['DATA_MODELS_FILE']} has
{len(pmcs)} PMCs"
diff --git a/atr/routes.py b/atr/routes.py
index a1be503..4b8268a 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -20,7 +20,6 @@
import asyncio
import datetime
import hashlib
-import json
import pprint
import secrets
import shutil
@@ -33,7 +32,6 @@ from typing import Any, cast
import aiofiles
import aiofiles.os
import gnupg
-import httpx
from quart import Request, current_app, render_template, request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -50,15 +48,12 @@ from asfquart.session import read as session_read
from .models import (
PMC,
- DistributionChannel,
Package,
PMCKeyLink,
- ProductLine,
PublicSigningKey,
Release,
ReleasePhase,
ReleaseStage,
- VotePolicy,
)
if APP is ...:
@@ -199,145 +194,6 @@ async def root_add_release_candidate() -> str:
)
[email protected]("/admin/database")
[email protected]("/admin/database/<model>")
-@require(R.committer)
-async def root_admin_database(model: str = "PMC") -> str:
- "Browse all records in the database."
- session = await session_read()
- if session is None:
- raise ASFQuartException("Not authenticated", errorcode=401)
-
- if session.uid not in ALLOWED_USERS:
- raise ASFQuartException("You are not authorized to browse data",
errorcode=403)
-
- # Map of model names to their classes
- models = {
- "PMC": PMC,
- "Release": Release,
- "Package": Package,
- "VotePolicy": VotePolicy,
- "ProductLine": ProductLine,
- "DistributionChannel": DistributionChannel,
- "PublicSigningKey": PublicSigningKey,
- "PMCKeyLink": PMCKeyLink,
- }
-
- if model not in models:
- # Default to PMC if invalid model specified
- model = "PMC"
-
- async_session = current_app.config["async_session"]
- async with async_session() as db_session:
- # Get all records for the selected model
- statement = select(models[model])
- records = (await db_session.execute(statement)).scalars().all()
-
- # Convert records to dictionaries for JSON serialization
- records_dict = []
- for record in records:
- if hasattr(record, "dict"):
- record_dict = record.dict()
- else:
- # Fallback for models without dict() method
- record_dict = {
- "id": getattr(record, "id", None),
- "storage_key": getattr(record, "storage_key", None),
- }
- for key in record.__dict__:
- if not key.startswith("_"):
- record_dict[key] = getattr(record, key)
- records_dict.append(record_dict)
-
- return await render_template("data-browser.html",
models=list(models.keys()), model=model, records=records_dict)
-
-
[email protected]("/admin/update-pmcs", methods=["GET", "POST"])
-@require(R.committer)
-async def root_admin_update_pmcs() -> str:
- "Update PMCs from remote, authoritative committee-info.json."
- # Check authentication
- session = await session_read()
- if session is None:
- raise ASFQuartException("Not authenticated", errorcode=401)
-
- if session.uid not in ALLOWED_USERS:
- raise ASFQuartException("You are not authorized to update PMCs",
errorcode=403)
-
- if request.method == "POST":
- # Fetch committee-info.json from Whimsy
- WHIMSY_URL = "https://whimsy.apache.org/public/committee-info.json"
- async with httpx.AsyncClient() as client:
- try:
- response = await client.get(WHIMSY_URL)
- response.raise_for_status()
- data = response.json()
- except (httpx.RequestError, json.JSONDecodeError) as e:
- raise ASFQuartException(f"Failed to fetch committee data:
{str(e)}", errorcode=500)
-
- committees = data.get("committees", {})
- updated_count = 0
-
- async_session = current_app.config["async_session"]
- async with async_session() as db_session:
- async with db_session.begin():
- for committee_id, info in committees.items():
- # Skip non-PMC committees
- if not info.get("pmc", False):
- continue
-
- # Get or create PMC
- statement = select(PMC).where(PMC.project_name ==
committee_id)
- pmc = (await
db_session.execute(statement)).scalar_one_or_none()
- if not pmc:
- pmc = PMC(project_name=committee_id)
- db_session.add(pmc)
-
- # Update PMC data
- roster = info.get("roster", {})
- # TODO: Here we say that roster == pmc_members ==
committers
- # We ought to do this more accurately instead
- pmc.pmc_members = list(roster.keys())
- pmc.committers = list(roster.keys())
-
- # Mark chairs as release managers
- # TODO: Who else is a release manager? How do we know?
- chairs = [m["id"] for m in info.get("chairs", [])]
- pmc.release_managers = chairs
-
- updated_count += 1
-
- # Add special entry for Tooling PMC
- # Not clear why, but it's not in the Whimsy data
- statement = select(PMC).where(PMC.project_name == "tooling")
- tooling_pmc = (await
db_session.execute(statement)).scalar_one_or_none()
- if not tooling_pmc:
- tooling_pmc = PMC(project_name="tooling")
- db_session.add(tooling_pmc)
- updated_count += 1
-
- # Update Tooling PMC data
- # Could put this in the "if not tooling_pmc" block, perhaps
- tooling_pmc.pmc_members = ["wave", "tn", "sbp"]
- tooling_pmc.committers = ["wave", "tn", "sbp"]
- tooling_pmc.release_managers = ["wave"]
-
- return f"Successfully updated {updated_count} PMCs from Whimsy"
-
- # For GET requests, show the update form
- return await render_template("update-pmcs.html")
-
-
[email protected]("/database/debug")
-async def root_database_debug() -> str:
- """Debug information about the database."""
- async_session = current_app.config["async_session"]
- async with async_session() as db_session:
- statement = select(PMC)
- pmcs = (await db_session.execute(statement)).scalars().all()
- return f"Database using {current_app.config['DATA_MODELS_FILE']} has
{len(pmcs)} PMCs"
-
-
@APP.route("/release/signatures/verify/<release_key>")
@require(R.committer)
async def root_release_signatures_verify(release_key: str) -> str:
@@ -485,12 +341,6 @@ async def root_pmc_list() -> list[dict]:
]
[email protected]("/secret")
-@require(R.committer)
-async def root_secret() -> str:
- return "Secret stuff!"
-
-
@APP.route("/user/keys/add", methods=["GET", "POST"])
@require(R.committer)
async def root_user_keys_add() -> str:
diff --git a/atr/server.py b/atr/server.py
index 83c938e..97314b3 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -29,6 +29,7 @@ import asfquart
import asfquart.generics
import asfquart.session
from asfquart.base import QuartApp
+from atr.blueprints import register_blueprints
from .models import __file__ as data_models_file
@@ -115,12 +116,15 @@ def create_app() -> QuartApp:
await conn.run_sync(SQLModel.metadata.create_all)
app.config["engine"] = engine
+ # TODO: apply this only for debug
+ app.config["TEMPLATES_AUTO_RELOAD"] = True
@app.after_serving
async def shutdown() -> None:
app.background_tasks.clear()
register_routes()
+ register_blueprints(app)
return app
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 671dab5..f429fe6 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -66,7 +66,7 @@ ul { padding-left: 1rem; }
label { font-weight: 500; border-bottom: 1px dashed #d1d2d3; padding-bottom:
0.5rem; }
-form { background-color: #ffe; border: 1px solid #ddb; padding: 1rem;
border-radius: 0.5rem; }
+form.striking { background-color: #ffe; border: 1px solid #ddb; padding: 1rem;
border-radius: 0.5rem; }
footer {
padding: 2rem;
diff --git a/atr/templates/add-release-candidate.html
b/atr/templates/add-release-candidate.html
index 84c1883..6186473 100644
--- a/atr/templates/add-release-candidate.html
+++ b/atr/templates/add-release-candidate.html
@@ -90,7 +90,7 @@
<h2>Select files</h2>
- <form method="post" enctype="multipart/form-data">
+ <form method="post" enctype="multipart/form-data" class="striking">
<table class="form-table">
<tbody>
<tr>
diff --git a/atr/templates/data-browser.html b/atr/templates/data-browser.html
index 7643704..61866bc 100644
--- a/atr/templates/data-browser.html
+++ b/atr/templates/data-browser.html
@@ -70,7 +70,7 @@
<div class="model-nav">
{% for model_name in models %}
- <a href="{{ url_for('root_admin_database', model=model_name) }}"
+ <a href="{{ url_for('secret_blueprint.secret_data', model=model_name) }}"
{% if model == model_name %}class="active"{% endif %}>{{ model_name
}}</a>
{% endfor %}
</div>
diff --git a/atr/templates/pages.html b/atr/templates/pages.html
index 787462a..4fc6127 100644
--- a/atr/templates/pages.html
+++ b/atr/templates/pages.html
@@ -83,7 +83,7 @@
<h3>
<a href="{{ url_for('root') }}">/</a>
</h3>
- <div class="endpoint-description">Simple welcome page with Apache
Trusted Releases title.</div>
+ <div class="endpoint-description">Main welcome page.</div>
<div class="endpoint-meta">
Access: <span class="access-requirement public">Public</span>
</div>
@@ -130,14 +130,6 @@
Access: <span class="access-requirement public">Public</span>
</div>
</div>
-
- <div class="endpoint">
- <h3>/pmc/create/<project_name></h3>
- <div class="endpoint-description">Create a new PMC with sample data
(JSON format).</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
</div>
<div class="endpoint-group">
@@ -191,11 +183,11 @@
<h3>
<a href="{{ url_for('root_user_keys_delete')
}}">/user/keys/delete</a>
</h3>
- <div class="endpoint-description">Debug endpoint to delete all GPG
keys associated with your account.</div>
+ <div class="endpoint-description">Delete all GPG keys associated with
your account.</div>
<div class="endpoint-meta">
Access: <span class="access-requirement committer">Committer</span>
<br />
- <span class="access-requirement warning">Warning: This is a debug
endpoint that immediately deletes all your keys without confirmation!</span>
+ <span class="access-requirement warning">Warning: This will delete
all your keys without confirmation!</span>
</div>
</div>
</div>
@@ -205,7 +197,7 @@
<div class="endpoint">
<h3>
- <a href="{{ url_for('root_admin_database') }}">/admin/database</a>
+ <a href="{{ url_for('secret_blueprint.secret_data')
}}">/secret/data</a>
</h3>
<div class="endpoint-description">Browse all records in the
database.</div>
<div class="endpoint-meta">
@@ -218,7 +210,7 @@
<div class="endpoint">
<h3>
- <a href="{{ url_for('root_admin_update_pmcs')
}}">/admin/update-pmcs</a>
+ <a href="{{ url_for('secret_blueprint.secret_pmcs_update')
}}">/secret/pmcs/update</a>
</h3>
<div class="endpoint-description">Update PMCs from remote,
authoritative committee-info.json.</div>
<div class="endpoint-meta">
@@ -229,30 +221,6 @@
</div>
</div>
</div>
-
- <div class="endpoint-group">
- <h2>Debug/Testing</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_secret') }}">/secret</a>
- </h3>
- <div class="endpoint-description">Test endpoint for
authentication.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- </div>
- </div>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_database_debug') }}">/database/debug</a>
- </h3>
- <div class="endpoint-description">Debug information about the
database.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
- </div>
</div>
{% endblock content %}
diff --git a/atr/templates/user-keys-add.html b/atr/templates/user-keys-add.html
index 7db121f..46b9741 100644
--- a/atr/templates/user-keys-add.html
+++ b/atr/templates/user-keys-add.html
@@ -188,7 +188,7 @@
</div>
{% endif %}
- <form method="post">
+ <form method="post" class="striking">
<div class="form-group">
<label for="public_key">Public Key:</label>
<textarea id="public_key"
diff --git a/typestubs/asfquart/auth.pyi b/typestubs/asfquart/auth.pyi
index 27c06a6..156cb6c 100644
--- a/typestubs/asfquart/auth.pyi
+++ b/typestubs/asfquart/auth.pyi
@@ -3,83 +3,82 @@ This type stub file was generated by pyright.
"""
import typing
+from typing import Any, Callable, Coroutine, Iterable, Optional, TypeVar,
Union, overload
from . import base, session
"""ASFQuart - Authentication methods and decorators"""
+T = TypeVar('T')
+P = TypeVar('P', bound=Callable[..., Coroutine[Any, Any, Any]])
+ReqFunc = Callable[[session.ClientSession], tuple[bool, str]]
+
class Requirements:
"""Various pre-defined access requirements"""
- E_NOT_LOGGED_IN = ...
- E_NOT_MEMBER = ...
- E_NOT_CHAIR = ...
- E_NO_MFA = ...
- E_NOT_ROOT = ...
- E_NOT_PMC = ...
- E_NOT_ROLEACCOUNT = ...
+ E_NOT_LOGGED_IN: str
+ E_NOT_MEMBER: str
+ E_NOT_CHAIR: str
+ E_NO_MFA: str
+ E_NOT_ROOT: str
+ E_NOT_PMC: str
+ E_NOT_ROLEACCOUNT: str
+
@classmethod
- def mfa_enabled(cls, client_session: session.ClientSession): # ->
tuple[bool, LiteralString]:
+ def mfa_enabled(cls, client_session: session.ClientSession) -> tuple[bool,
str]:
"""Tests for MFA enabled in the client session"""
...
+
@classmethod
- def committer(
- cls, client_session: session.ClientSession
- ): # -> tuple[bool, Literal['You need to be logged in to access this
endpoint.']]:
+ def committer(cls, client_session: session.ClientSession) -> tuple[bool,
str]:
"""Tests for whether the user is a committer on any project"""
...
+
@classmethod
- def member(cls, client_session: session.ClientSession): # -> tuple[bool,
LiteralString]:
+ def member(cls, client_session: session.ClientSession) -> tuple[bool, str]:
"""Tests for whether the user is a foundation member"""
...
+
@classmethod
- def chair(cls, client_session: session.ClientSession): # -> tuple[bool,
LiteralString]:
+ def chair(cls, client_session: session.ClientSession) -> tuple[bool, str]:
"""tests for whether the user is a chair of any top-level project"""
...
+
@classmethod
- def root(cls, client_session: session.ClientSession): # -> tuple[bool,
LiteralString]:
+ def root(cls, client_session: session.ClientSession) -> tuple[bool, str]:
"""tests for whether the user is a member of infra-root"""
...
+
@classmethod
- def pmc_member(cls, client_session: session.ClientSession): # ->
tuple[bool, LiteralString]:
+ def pmc_member(cls, client_session: session.ClientSession) -> tuple[bool,
str]:
"""tests for whether the user is a PMC member of any top-level
project"""
...
+
@classmethod
- def roleacct(
- cls, client_session: session.ClientSession
- ): # -> tuple[bool, Literal['This endpoint is only accessible to role
accounts.']]:
+ def roleacct(cls, client_session: session.ClientSession) -> tuple[bool,
str]:
"""tests for whether the user is a service account"""
...
class AuthenticationFailed(base.ASFQuartException):
def __init__(self, message: str = ..., errorcode: int = ...) -> None: ...
-def requirements_to_iter(args: typing.Any): # -> list[Any] | Any |
Iterable[Any]:
+def requirements_to_iter(args: Any) -> Iterable[Any]:
"""Converts any auth req args (single arg, list, tuple) to an iterable if
not already one"""
...
+@overload
+def require(func: P) -> P: ...
+
+@overload
def require(
- func: typing.Optional[typing.Callable] = ...,
- all_of: typing.Optional[typing.Iterable] = ...,
- any_of: typing.Optional[typing.Iterable] = ...,
-):
- """Adds authentication/authorization requirements to an endpoint. Can be a
single requirement or a list
- of requirements. By default, all requirements must be satisfied, though
this can be made optional by
- explicitly using the `all_of` or `any_of` keywords to specify optionality.
Requirements must be part
- of the asfquart.auth.Requirements class, which consists of the following
test:
-
- - mfa_enabled: The client must authenticate with a method that has MFA
enabled
- - committer: The client must be a committer
- - member: The client must be a foundation member
- - chair: The client must be a chair of a project
-
- In addition, any endpoint decorated with @require will implicitly require
ANY form of
- authenticated session. This is mandatory and also works as a bare
decorator.
-
- Examples:
- @require(Requirements.member) # Require session, require ASF member
- @require # Require any authed session
- @require({Requirements.mfa_enabled, Requirements.chair}) # Require
any project chair with MFA-enabled session
- @require(all_of=Requirements.mfa_enabled, any_of={Requirements.member,
Requirements.chair})
- # Require either ASF member OR project chair, but also require MFA
enabled in any case.
- """
- ...
+ func: Optional[ReqFunc] = None,
+ all_of: Optional[Union[ReqFunc, Iterable[ReqFunc]]] = None,
+ any_of: Optional[Union[ReqFunc, Iterable[ReqFunc]]] = None,
+) -> Callable[[P], P]: ...
+
+@overload
+def require(
+ func: Union[Callable[..., tuple[bool, str]], Iterable[Callable[...,
tuple[bool, str]]]] = None,
+ *,
+ all_of: Optional[Union[Callable[..., tuple[bool, str]],
Iterable[Callable[..., tuple[bool, str]]]]] = None,
+ any_of: Optional[Union[Callable[..., tuple[bool, str]],
Iterable[Callable[..., tuple[bool, str]]]]] = None,
+) -> Callable[[P], P]: ...
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]