🛂 — Allow partial or total anonymous access to web interface (fix #63)

This commit is contained in:
Luc Didry 2024-11-28 11:48:08 +01:00
parent 841f8638de
commit da221b856b
No known key found for this signature in database
GPG key ID: EA868E12D0257E3C
8 changed files with 103 additions and 7 deletions

View file

@ -13,6 +13,7 @@
BREAKING CHANGE: `mo` is no longer accepted for declaring a duration in month in the configuration
You need to use `M`, `month` or `months`
- ✨ - Allow to choose a frequency smaller than a minute
- ✨🛂 — Allow partial or total anonymous access to web interface (#63)
## 0.5.0

View file

@ -1,5 +1,7 @@
---
general:
# Except for frequency and recheck_delay settings, changes in general
# section of the configuration will need a restart of argos server.
db:
# The database URL, as defined in SQLAlchemy docs :
# https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
@ -28,6 +30,12 @@ general:
# If not present, the "Remember me" feature is not available
# remember_me_duration: "1M"
# Unauthenticated access
# If can grant an unauthenticated access to the dashboard or to all pages
# To do so, choose either "dashboard", or "all"
# If not present, all pages needs authentication
# unauthenticated_access: "all"
# Default delay for checks.
# Can be superseeded in domain configuration.
# For ex., to run checks every 5 minutes:

View file

@ -27,6 +27,7 @@ from argos.schemas.utils import Method
Severity = Literal["warning", "error", "critical", "unknown"]
Environment = Literal["dev", "test", "production"]
Unauthenticated = Literal["dashboard", "all"]
SQLiteDsn = Annotated[
Url,
UrlConstraints(
@ -190,6 +191,7 @@ class General(BaseModel):
cookie_secret: str
session_duration: int = 10080 # 7 days
remember_me_duration: int | None = None
unauthenticated_access: Unauthenticated | None = None
frequency: float
recheck_delay: float | None = None
root_path: str = ""

View file

@ -100,7 +100,7 @@ def setup_database(appli):
models.Base.metadata.create_all(bind=engine)
def create_manager(cookie_secret):
def create_manager(cookie_secret: str) -> LoginManager:
if cookie_secret == "foo_bar_baz":
logger.warning(
"You should change the cookie_secret secret in your configuration file."

View file

@ -18,6 +18,9 @@ def get_config(request: Request):
async def get_manager(request: Request) -> LoginManager:
if request.app.state.config.general.unauthenticated_access is not None:
return await request.app.state.manager.optional(request)
return await request.app.state.manager(request)

View file

@ -17,6 +17,7 @@ from sqlalchemy.orm import Session
from argos.checks.base import Status
from argos.schemas import Config
from argos.server import queries
from argos.server.exceptions import NotAuthenticatedException
from argos.server.models import Result, Task, User
from argos.server.routes.dependencies import get_config, get_db, get_manager
@ -33,6 +34,12 @@ async def login_view(
msg: str | None = None,
config: Config = Depends(get_config),
):
if config.general.unauthenticated_access == "all":
return RedirectResponse(
request.url_for("get_severity_counts_view"),
status_code=status.HTTP_303_SEE_OTHER,
)
token = request.cookies.get("access-token")
if token is not None and token != "":
manager = request.app.state.manager
@ -66,6 +73,12 @@ async def post_login(
rememberme: Annotated[str | None, Form()] = None,
config: Config = Depends(get_config),
):
if config.general.unauthenticated_access == "all":
return RedirectResponse(
request.url_for("get_severity_counts_view"),
status_code=status.HTTP_303_SEE_OTHER,
)
username = data.username
user = await queries.get_user(db, username)
invalid_credentials = templates.TemplateResponse(
@ -103,7 +116,17 @@ async def post_login(
@route.get("/logout")
async def logout_view(request: Request, user: User | None = Depends(get_manager)):
async def logout_view(
request: Request,
config: Config = Depends(get_config),
user: User | None = Depends(get_manager),
):
if config.general.unauthenticated_access == "all":
return RedirectResponse(
request.url_for("get_severity_counts_view"),
status_code=status.HTTP_303_SEE_OTHER,
)
response = RedirectResponse(
request.url_for("login_view").include_query_params(msg="logout"),
status_code=status.HTTP_303_SEE_OTHER,
@ -133,6 +156,7 @@ async def get_severity_counts_view(
"agents": agents,
"auto_refresh_enabled": auto_refresh_enabled,
"auto_refresh_seconds": auto_refresh_seconds,
"user": user,
},
)
@ -141,9 +165,14 @@ async def get_severity_counts_view(
async def get_domains_view(
request: Request,
user: User | None = Depends(get_manager),
config: Config = Depends(get_config),
db: Session = Depends(get_db),
):
"""Show all tasks and their current state"""
if config.general.unauthenticated_access == "dashboard":
if user is None:
raise NotAuthenticatedException
tasks = db.query(Task).all()
domains_severities = defaultdict(list)
@ -184,6 +213,7 @@ async def get_domains_view(
"last_checks": domains_last_checks,
"total_task_count": len(tasks),
"agents": agents,
"user": user,
},
)
@ -193,12 +223,23 @@ async def get_domain_tasks_view(
request: Request,
domain: str,
user: User | None = Depends(get_manager),
config: Config = Depends(get_config),
db: Session = Depends(get_db),
):
"""Show all tasks attached to a domain"""
if config.general.unauthenticated_access == "dashboard":
if user is None:
raise NotAuthenticatedException
tasks = db.query(Task).filter(Task.domain.contains(f"//{domain}")).all()
return templates.TemplateResponse(
"domain.html", {"request": request, "domain": domain, "tasks": tasks}
"domain.html",
{
"request": request,
"domain": domain,
"tasks": tasks,
"user": user,
},
)
@ -207,12 +248,23 @@ async def get_result_view(
request: Request,
result_id: int,
user: User | None = Depends(get_manager),
config: Config = Depends(get_config),
db: Session = Depends(get_db),
):
"""Show the details of a result"""
if config.general.unauthenticated_access == "dashboard":
if user is None:
raise NotAuthenticatedException
result = db.query(Result).get(result_id)
return templates.TemplateResponse(
"result.html", {"request": request, "result": result, "error": Status.ERROR}
"result.html",
{
"request": request,
"result": result,
"error": Status.ERROR,
"user": user,
},
)
@ -225,6 +277,10 @@ async def get_task_results_view(
config: Config = Depends(get_config),
):
"""Show history of a tasks results"""
if config.general.unauthenticated_access == "dashboard":
if user is None:
raise NotAuthenticatedException
results = (
db.query(Result)
.filter(Result.task_id == task_id)
@ -243,6 +299,7 @@ async def get_task_results_view(
"task": task,
"description": description,
"error": Status.ERROR,
"user": user,
},
)
@ -251,9 +308,14 @@ async def get_task_results_view(
async def get_agents_view(
request: Request,
user: User | None = Depends(get_manager),
config: Config = Depends(get_config),
db: Session = Depends(get_db),
):
"""Show argos agents and the last time the server saw them"""
if config.general.unauthenticated_access == "dashboard":
if user is None:
raise NotAuthenticatedException
last_seen = (
db.query(Result.agent_id, func.max(Result.submitted_at).label("submitted_at"))
.group_by(Result.agent_id)
@ -261,7 +323,12 @@ async def get_agents_view(
)
return templates.TemplateResponse(
"agents.html", {"request": request, "last_seen": last_seen}
"agents.html",
{
"request": request,
"last_seen": last_seen,
"user": user,
},
)

View file

@ -63,6 +63,8 @@
Agents
</a>
</li>
{% set unauthenticated_access = request.app.state.config.general.unauthenticated_access %}
{% if (user is defined and user is not none) or unauthenticated_access == "all" %}
<li>
<a href="#"
id="reschedule-all"
@ -72,13 +74,24 @@
Reschedule non-ok checks
</a>
</li>
{% endif %}
{% if user is defined and user is not none %}
<li>
<a href="{{ url_for('logout_view') }}"
class="outline {{ 'contrast' if request.url == url_for('get_agents_view') }}"
class="outline }}"
role="button">
Logout
</a>
</li>
{% elif unauthenticated_access != "all" %}
<li>
<a href="{{ url_for('login_view') }}"
class="outline }}"
role="button">
Login
</a>
</li>
{% endif %}
</ul>
</details>
</li>

View file

@ -276,7 +276,9 @@ Options:
### Server user management
To access Argos web interface, you need to create at least one user.
You can choose to protect Argos web interface with a user system, in which case youll need to create at least one user.
See [`unauthenticated_access` in the configuration file](configuration.md) to allow partial or total unauthenticated access to Argos.
You can manage users only through CLI.