diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d3c62..75fdaf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/argos/config-example.yaml b/argos/config-example.yaml index 8dd096d..3e713ba 100644 --- a/argos/config-example.yaml +++ b/argos/config-example.yaml @@ -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: diff --git a/argos/schemas/config.py b/argos/schemas/config.py index e410c4a..3bd5c62 100644 --- a/argos/schemas/config.py +++ b/argos/schemas/config.py @@ -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 = "" diff --git a/argos/server/main.py b/argos/server/main.py index b6ee412..fc3957b 100644 --- a/argos/server/main.py +++ b/argos/server/main.py @@ -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." diff --git a/argos/server/routes/dependencies.py b/argos/server/routes/dependencies.py index f26d5ee..3f3b6c5 100644 --- a/argos/server/routes/dependencies.py +++ b/argos/server/routes/dependencies.py @@ -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) diff --git a/argos/server/routes/views.py b/argos/server/routes/views.py index 4028555..d320e1d 100644 --- a/argos/server/routes/views.py +++ b/argos/server/routes/views.py @@ -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 task’s 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, + }, ) diff --git a/argos/server/templates/base.html b/argos/server/templates/base.html index 4964031..c88065f 100644 --- a/argos/server/templates/base.html +++ b/argos/server/templates/base.html @@ -63,6 +63,8 @@ Agents + {% set unauthenticated_access = request.app.state.config.general.unauthenticated_access %} + {% if (user is defined and user is not none) or unauthenticated_access == "all" %}
  • + {% endif %} + {% if user is defined and user is not none %}
  • Logout
  • + {% elif unauthenticated_access != "all" %} +
  • + + Login + +
  • + {% endif %} diff --git a/docs/cli.md b/docs/cli.md index 0435506..933aba1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 you’ll 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.