diff --git a/.gitignore b/.gitignore index 2be0351..57bdb06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc +.env /__pycache__/ diff --git a/README.md b/README.md index 8643e7f..89e4288 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ No Masters edition of Keycloak. - They are valid for 30 days by default (configurable via `INVITE_TIME_LIMIT`) - Anyone with an invite link can create an account on the Keycloak, so don't share publicly! - There is no access granularity on the account creation implemented yet, so the accounts are "global" + - **New**: it is possible to only allow "admins" to log in, see [feature flags](#feature-flags) - Once the user fills in their name, email, password they will receive an email verification mail If you want a feature implemented, please open an issue to discuss. @@ -60,6 +61,32 @@ your technology stack. - Log in with your usual login details - Follow the instructions on the web page to perform administrative actions +## Feature Flags + +### Only admins can log in + +#### Keycloak + +- Create a new group under `Groups` called `Administrators` (case sensistive!) +- Create a new scope under `Client scopes` + - Name: `groups` + - Type: `Optional` + - Include in token scope: `yes` +- Under the `Mappers` tab of this client scope, choose `Add mapper` + - Mapper type/Name: `Groups Membership` + - Token claim name: `groups` + - Add to ID token: `yes` + - Add to access token: `yes` + - Add to userinfo: `yes` +- Add this client scope to your `admin-cli` client as `Optional` +- Add a test user to this group under `Users` + +#### Keycloak Community Portal + +- Set `FEATURE_FLAG_ADMINS_ONLY=True` in your `.env` +- You may want to customise `KEYCLOAK_GROUPS_KEY` / `KEYCLOAK_ADMINS_GROUP` if + you changed the value of `groups` / `Administrators` above + ## Hacking It's a [FastAPI](https://fastapi.tiangolo.com/) application (if you know diff --git a/keycloak_collective_portal/config.py b/keycloak_collective_portal/config.py index 8cb53de..00afd85 100644 --- a/keycloak_collective_portal/config.py +++ b/keycloak_collective_portal/config.py @@ -12,7 +12,9 @@ KEYCLOAK_CLIENT_ID = environ.get("KEYCLOAK_CLIENT_ID") KEYCLOAK_CLIENT_SECRET = environ.get("KEYCLOAK_CLIENT_SECRET") KEYCLOAK_DOMAIN = environ.get("KEYCLOAK_DOMAIN") KEYCLOAK_REALM = environ.get("KEYCLOAK_REALM") -KEYCLOAK_SCOPES = environ.get("KEYCLOAK_SCOPES", "openid profile email") +KEYCLOAK_SCOPES = environ.get("KEYCLOAK_SCOPES", "openid profile email groups") +KEYCLOAK_GROUPS_KEY = environ.get("KEYCLOAK_GROUPS_KEY", "groups") +KEYCLOAK_ADMINS_GROUP = environ.get("KEYCLOAK_ADMINS_GROUP", "Administrators") KEYCLOAK_BASE_URL = f"https://{KEYCLOAK_DOMAIN}/realms/{KEYCLOAK_REALM}/protocol/openid-connect" # noqa # Redis connection details, our main storage @@ -39,5 +41,15 @@ elif LOG_LEVEL == "debug": else: APP_LOG_LEVEL = logging.INFO + +def to_bool(env_var): + """Parse a bool from the environment.""" + return environ.get(env_var, "False").lower() in ("true", "1", "t") + + # Automatically log folks in or show the default log in page? -AUTOMATICALLY_LOG_IN = environ.get("AUTOMATICALLY_LOG_IN", False) +AUTOMATICALLY_LOG_IN = to_bool("AUTOMATICALLY_LOG_IN") + +# Feature flags +# Only admins can log in to the interface +FEATURE_FLAG_ADMINS_ONLY = to_bool("FEATURE_FLAG_ADMINS_ONLY") diff --git a/keycloak_collective_portal/routes/root.py b/keycloak_collective_portal/routes/root.py index eb2a806..1785fbc 100644 --- a/keycloak_collective_portal/routes/root.py +++ b/keycloak_collective_portal/routes/root.py @@ -15,7 +15,26 @@ router = APIRouter() async def home( request: Request, user=Depends(get_user), invites=Depends(get_invites) ): + from keycloak_collective_portal.config import ( + FEATURE_FLAG_ADMINS_ONLY, + KEYCLOAK_ADMINS_GROUP, + KEYCLOAK_GROUPS_KEY, + ) + context = {"request": request, "user": user, "invites": invites} + + if FEATURE_FLAG_ADMINS_ONLY: + context["message"] = "only admins can access this service" + if KEYCLOAK_GROUPS_KEY not in user: + return request.app.state.templates.TemplateResponse( + "invalid.html", context=context + ) + + if KEYCLOAK_ADMINS_GROUP not in user[KEYCLOAK_GROUPS_KEY]: + return request.app.state.templates.TemplateResponse( + "invalid.html", context=context + ) + return request.app.state.templates.TemplateResponse( "admin.html", context=context )