From 06d62606f51cd379fc3f86940d2ceacd5eea86df Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Mon, 13 May 2024 10:51:18 -0400 Subject: [PATCH 1/9] Add workflow dispatch trigger. --- .github/workflows/publish-docker-image.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-docker-image.yaml b/.github/workflows/publish-docker-image.yaml index 94b134f..1962e41 100644 --- a/.github/workflows/publish-docker-image.yaml +++ b/.github/workflows/publish-docker-image.yaml @@ -8,6 +8,7 @@ on: pull_request: paths: - docker/** + workflow_dispatch: env: REGISTRY: ghcr.io From 20f646207b99b7cade249cb101b9ec8e22129124 Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Thu, 27 Jun 2024 19:40:00 -0500 Subject: [PATCH 2/9] Refactor coupon configuration to allow for multiple coupons with different configuration --- self-registration/app/main.py | 57 +++++------ .../__init__.py | 24 +++-- .../terraform/main.tf | 11 +-- .../terraform/modules/keycloak/main.tf | 20 ++-- .../terraform/modules/keycloak/variables.tf | 2 +- .../self-registration/chart/values.yaml | 6 +- .../modules/self-registration/main.tf | 18 ++-- .../modules/self-registration/variables.tf | 25 ++--- .../terraform/variables.tf | 30 ++---- .../tests/unit/test_plugin.py | 97 ++++++++++++------- 10 files changed, 146 insertions(+), 144 deletions(-) diff --git a/self-registration/app/main.py b/self-registration/app/main.py index 26a20c3..d2496a3 100644 --- a/self-registration/app/main.py +++ b/self-registration/app/main.py @@ -5,10 +5,11 @@ from datetime import datetime, timedelta import yaml -from fastapi import APIRouter, FastAPI, Form, Request +from fastapi import FastAPI, Form, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from keycloak import KeycloakAdmin, KeycloakConnectionError, KeycloakGetError + from theme import DEFAULT_THEME @@ -36,8 +37,7 @@ class UserExistsException(Exception): config = {} -def check_email_domain(email): - approved_domains = config.get("approved_domains", []) +def check_email_domain(email, approved_domains): for domain in approved_domains: # Replace wildcard with its regex equivalent pattern = domain.replace("*", ".*") @@ -96,29 +96,30 @@ def generate_random_password(length=12): # Function to assign a user to a group -def assign_user_to_group(user, group_name): - try: - keycloak_admin = KeycloakAdmin( - server_url=config["keycloak"]["server_url"], - realm_name=config["keycloak"]["realm_name"], - client_id=config["keycloak"]["client_id"], - client_secret_key=config["keycloak"]["client_secret"], - user_realm_name=config["keycloak"]["realm_name"], - verify=True, - ) - except KeycloakConnectionError: - return False +def assign_user_to_groups(user, groups): + for group_name in groups: + try: + keycloak_admin = KeycloakAdmin( + server_url=config["keycloak"]["server_url"], + realm_name=config["keycloak"]["realm_name"], + client_id=config["keycloak"]["client_id"], + client_secret_key=config["keycloak"]["client_secret"], + user_realm_name=config["keycloak"]["realm_name"], + verify=True, + ) + except KeycloakConnectionError: + return False - # Get group - try: - group = keycloak_admin.get_group_by_path(group_name) - except KeycloakGetError: - return False # Fail if Keycloak group throws exception finding group - if not group: - return False # Also fail if Keycloak admin doesn't throw exception but group is still missing + # Get group + try: + group = keycloak_admin.get_group_by_path(group_name) + except KeycloakGetError: + return False # Fail if Keycloak group throws exception finding group + if not group: + return False # Also fail if Keycloak admin doesn't throw exception but group is still missing - # Assign the user to the group - keycloak_admin.group_user_add(user["id"], group["id"]) + # Assign the user to the group + keycloak_admin.group_user_add(user["id"], group["id"]) return True @@ -156,20 +157,20 @@ def read_root(request: Request): @app.post(url_prefix + "/validate/") async def validate_submission(request: Request, email: str = Form(...), coupon_code: str = Form(...)): - if coupon_code in config.get("coupons", []): - if check_email_domain(email): + if coupon_config := config.get("coupons", {}).get(coupon_code): + if check_email_domain(email, coupon_config.get("approved_domains", [])): # Create the user in Keycloak try: user, temporary_password, expiration_date = create_keycloak_user( - email, config.get("account_expiration_days", None) + email, coupon_config.get("account_expiration_days", None) ) except UserExistsException as e: return templates.TemplateResponse("index.html", get_template_context(request, str(e))) # Assign user to group if user: - success = assign_user_to_group(user, config.get("registration_group", None)) + success = assign_user_to_groups(user, coupon_config.get("registration_groups", [])) if success: return templates.TemplateResponse( diff --git a/src/nebari_plugin_self_registration/__init__.py b/src/nebari_plugin_self_registration/__init__.py index 68ccb71..aa82502 100644 --- a/src/nebari_plugin_self_registration/__init__.py +++ b/src/nebari_plugin_self_registration/__init__.py @@ -20,19 +20,23 @@ class SelfRegistrationAffinitySelectorConfig(Base): app: Optional[str] = "" job: Optional[str] = "" + class SelfRegistrationAffinityConfig(Base): enabled: Optional[bool] = True selector: Union[SelfRegistrationAffinitySelectorConfig, str] = "general" +class SelfRegistrationCouponConfig(Base): + account_expiration_days: Optional[int] = 7 + approved_domains: Optional[List[str]] = [] + registration_groups: Optional[List[str]] = [] + + class SelfRegistrationConfig(Base): name: Optional[str] = "self-registration" namespace: Optional[str] = None values: Optional[Dict[str, Any]] = {} - account_expiration_days: Optional[int] = 7 - approved_domains: Optional[List[str]] = [] - coupons: Optional[List[str]] = [] - registration_group: Optional[str] = "" + coupons: Optional[Dict[str, SelfRegistrationCouponConfig]] = {} registration_message: Optional[str] = "" affinity: SelfRegistrationAffinityConfig = SelfRegistrationAffinityConfig() @@ -141,12 +145,14 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): chart_ns = self.config.namespace create_ns = False + try: + theme = self.config.theme.jupyterhub.dict() + except AttributeError: + theme = {} + return { "chart_name": self.config.self_registration.name, - "account_expiration_days": self.config.self_registration.account_expiration_days, - "approved_domains": self.config.self_registration.approved_domains, - "coupons": self.config.self_registration.coupons, - "registration_group": self.config.self_registration.registration_group, + "coupons": self.config.self_registration.model_dump()["coupons"], # serialize nested objects using model_dump() "registration_message": self.config.self_registration.registration_message, "project_name": self.config.escaped_project_name, "realm_id": keycloak_config["realm_id"], @@ -168,7 +174,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): ), }, "cloud_provider": self.config.provider, - "theme": self.config.theme.jupyterhub.dict(), + "theme": theme, } def get_keycloak_config(self, stage_outputs: Dict[str, Dict[str, Any]]): diff --git a/src/nebari_plugin_self_registration/terraform/main.tf b/src/nebari_plugin_self_registration/terraform/main.tf index 3d76909..9efa45b 100644 --- a/src/nebari_plugin_self_registration/terraform/main.tf +++ b/src/nebari_plugin_self_registration/terraform/main.tf @@ -5,22 +5,19 @@ locals { module "keycloak" { source = "./modules/keycloak" - realm_id = var.realm_id - client_id = var.client_id - base_url = var.base_url + realm_id = var.realm_id + client_id = var.client_id + base_url = var.base_url } module "self-registration" { source = "./modules/self-registration" - approved_domains = var.approved_domains - account_expiration_days = var.account_expiration_days chart_name = var.chart_name coupons = var.coupons create_namespace = var.create_namespace ingress_host = var.ingress_host self_registration_sa_name = local.self_registration_sa_name - registration_group = var.registration_group registration_message = var.registration_message namespace = var.namespace keycloak_base_url = var.external_url @@ -30,4 +27,4 @@ module "self-registration" { affinity = var.affinity cloud_provider = var.cloud_provider theme = var.theme -} \ No newline at end of file +} diff --git a/src/nebari_plugin_self_registration/terraform/modules/keycloak/main.tf b/src/nebari_plugin_self_registration/terraform/modules/keycloak/main.tf index c7e4138..eb700d7 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/keycloak/main.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/keycloak/main.tf @@ -2,13 +2,13 @@ locals { } resource "keycloak_openid_client" "this" { - realm_id = var.realm_id - name = var.client_id - client_id = var.client_id - access_type = "CONFIDENTIAL" - base_url = var.base_url - enabled = true - service_accounts_enabled = true + realm_id = var.realm_id + name = var.client_id + client_id = var.client_id + access_type = "CONFIDENTIAL" + base_url = var.base_url + enabled = true + service_accounts_enabled = true } # Get manage-users role via data and assign it to registration client service account @@ -26,7 +26,7 @@ data "keycloak_role" "manage_users" { resource "keycloak_openid_client_service_account_role" "registration_service_account_role" { realm_id = var.realm_id service_account_user_id = keycloak_openid_client.this.service_account_user_id - # Need to source as data? - client_id = data.keycloak_openid_client.realm_management.id - role = data.keycloak_role.manage_users.name + # Need to source as data? + client_id = data.keycloak_openid_client.realm_management.id + role = data.keycloak_role.manage_users.name } diff --git a/src/nebari_plugin_self_registration/terraform/modules/keycloak/variables.tf b/src/nebari_plugin_self_registration/terraform/modules/keycloak/variables.tf index a095b0f..6931a80 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/keycloak/variables.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/keycloak/variables.tf @@ -11,4 +11,4 @@ variable "client_id" { variable "base_url" { description = "Default URL to use when the auth server needs to redirect or link back to the client" type = string -} \ No newline at end of file +} diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml index 318c2a6..711b6f8 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml @@ -99,15 +99,11 @@ job: securityContext: {} resources: {} affinity: {} - app_configuration: - account_expiration_days: 7 - approved_domains: [] - coupons: [] + coupons: {} keycloak: server_url: "http://server.com/auth" realm_name: "my-realm" client_id: "self-registration" client_secret: "" - registration_group: "" diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf b/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf index 0ad94cf..50fddb4 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf @@ -5,7 +5,7 @@ locals { { for k in ["default", "app", "job"] : k => length(var.affinity.selector[k]) > 0 ? var.affinity.selector[k] : var.affinity.selector.default }, { app = var.affinity.selector - job = var.affinity.selector + job = var.affinity.selector }, ) } : { @@ -14,8 +14,9 @@ locals { } affinity_selector_key = { - aws = "eks.amazonaws.com/nodegroup" - gcp = "cloud.google.com/gke-nodepool" + aws = "eks.amazonaws.com/nodegroup" + gcp = "cloud.google.com/gke-nodepool" + local = "node-role.kubernetes.io/control-plane" } } @@ -80,22 +81,19 @@ resource "helm_release" "self_registration" { name = var.self_registration_sa_name } app_configuration = { - coupons = var.coupons - approved_domains = var.approved_domains - account_expiration_days = var.account_expiration_days - registration_group = var.registration_group - registration_message = var.registration_message + coupons = var.coupons + registration_message = var.registration_message keycloak = { server_url = var.keycloak_base_url realm_name = var.realm_id client_id = var.keycloak_config["client_id"] client_secret = var.keycloak_config["client_secret"] } - theme = var.theme + theme = var.theme } env = [ ] }), yamlencode(var.overrides), ] -} \ No newline at end of file +} diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf b/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf index 421b272..0e6e946 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf @@ -3,19 +3,14 @@ variable "chart_name" { type = string } -variable "account_expiration_days" { - description = "Days a self-registered account remains active before expiring." - type = number -} - -variable "approved_domains" { - description = "Approved email domains for user self registration" - type = list(string) -} - variable "coupons" { - description = "Valid coupons for user self registration" - type = list(string) + description = "Coupon configuration for user self registration" + type = map(object({ + account_expiration_days = number + approved_domains = list(string) + registration_groups = list(string) + })) + default = {} } variable "create_namespace" { @@ -46,11 +41,6 @@ variable "realm_id" { type = string } -variable "registration_group" { - description = "Name of Keycloak group to add registering users" - type = string -} - variable "registration_message" { description = "Custom message to display to registering users" type = string @@ -84,7 +74,6 @@ variable "affinity" { } } - variable "cloud_provider" { type = string } diff --git a/src/nebari_plugin_self_registration/terraform/variables.tf b/src/nebari_plugin_self_registration/terraform/variables.tf index ae81d66..8c98961 100644 --- a/src/nebari_plugin_self_registration/terraform/variables.tf +++ b/src/nebari_plugin_self_registration/terraform/variables.tf @@ -50,28 +50,14 @@ variable "overrides" { default = {} } -variable "account_expiration_days" { - description = "Days a self-registered account remains active before expiring." - type = number - default = 7 -} - -variable "approved_domains" { - description = "Approved email domains for user self registration" - type = list(string) - default = [] -} - variable "coupons" { - description = "Valid coupons for user self registration" - type = list(string) - default = [] -} - -variable "registration_group" { - description = "Name of Keycloak group to add registering users" - type = string - default = "" + description = "Coupon configuration for user self registration" + type = map(object({ + account_expiration_days = number + approved_domains = list(string) + registration_groups = list(string) + })) + default = {} } variable "registration_message" { @@ -97,7 +83,6 @@ variable "affinity" { } } - # GENERAL SETTINGS # ----------------- variable "cloud_provider" { @@ -109,5 +94,4 @@ variable "theme" { description = "Theme configured in theme.jupyterhub" type = map(any) default = {} - } diff --git a/src/nebari_plugin_self_registration/tests/unit/test_plugin.py b/src/nebari_plugin_self_registration/tests/unit/test_plugin.py index 8d207d2..f500308 100644 --- a/src/nebari_plugin_self_registration/tests/unit/test_plugin.py +++ b/src/nebari_plugin_self_registration/tests/unit/test_plugin.py @@ -1,5 +1,12 @@ import pytest -from nebari_plugin_self_registration import SelfRegistrationStage, SelfRegistrationConfig, InputSchema + +from nebari_plugin_self_registration import ( + InputSchema, + SelfRegistrationConfig, + SelfRegistrationCouponConfig, + SelfRegistrationStage, +) + class TestConfig(InputSchema): __test__ = False @@ -7,30 +14,50 @@ class TestConfig(InputSchema): domain: str escaped_project_name: str = "" provider: str = "local" - self_registration: SelfRegistrationConfig = SelfRegistrationConfig(account_expiration_days = 14, approved_domains = ["test1.com","test2.org"], coupons = ["COUPON1","COUPON2"], registration_group = "test_group") + self_registration: SelfRegistrationConfig = SelfRegistrationConfig( + coupons={ + "COUPON1": SelfRegistrationCouponConfig( + account_expiration_days=7, + approved_domains=["test1.com"], + registration_groups=["test_group"], + ), + "COUPON2": SelfRegistrationCouponConfig( + account_expiration_days=14, + approved_domains=["test2.com", "test2.org"], + registration_groups=["test_group", "test_group2"], + ), + }, + ) + @pytest.fixture(autouse=True) def mock_keycloak_connection(monkeypatch): - monkeypatch.setattr("nebari_plugin_self_registration.SelfRegistrationStage._attempt_keycloak_connection", lambda *args, **kwargs: True) + monkeypatch.setattr( + "nebari_plugin_self_registration.SelfRegistrationStage._attempt_keycloak_connection", + lambda *args, **kwargs: True, + ) + def test_ctor(): - sut = SelfRegistrationStage(output_directory = None, config = None) + sut = SelfRegistrationStage(output_directory=None, config=None) assert sut.name == "self-registration" assert sut.priority == 103 -def test_input_vars(): - config = TestConfig(namespace = "nebari-ns", domain = "my-test-domain.com", escaped_project_name="testprojectname") - sut = SelfRegistrationStage(output_directory = None, config = config) +def test_input_vars(): + config = TestConfig(namespace="nebari-ns", domain="my-test-domain.com", escaped_project_name="testprojectname") + sut = SelfRegistrationStage(output_directory=None, config=config) stage_outputs = get_stage_outputs() sut.check(stage_outputs) result = sut.input_vars(stage_outputs) assert result["chart_name"] == "self-registration" - assert result["account_expiration_days"] == 14 - assert result["approved_domains"] == ["test1.com","test2.org"] - assert result["coupons"] == ["COUPON1","COUPON2"] - assert result["registration_group"] == "test_group" + assert result["coupons"]["COUPON1"]["account_expiration_days"] == 7 + assert result["coupons"]["COUPON1"]["approved_domains"] == ["test1.com"] + assert result["coupons"]["COUPON1"]["registration_groups"] == ["test_group"] + assert result["coupons"]["COUPON2"]["account_expiration_days"] == 14 + assert result["coupons"]["COUPON2"]["approved_domains"] == ["test2.com", "test2.org"] + assert result["coupons"]["COUPON2"]["registration_groups"] == ["test_group", "test_group2"] assert result["project_name"] == "testprojectname" assert result["realm_id"] == "test-realm" assert result["client_id"] == "self-registration" @@ -41,42 +68,50 @@ def test_input_vars(): assert result["ingress_host"] == "my-test-domain.com" assert result["overrides"] == {} + def test_default_namespace(): - config = TestConfig(namespace = "nebari-ns", domain = "my-test-domain.com", provider="aws") - sut = SelfRegistrationStage(output_directory = None, config = config) + config = TestConfig(namespace="nebari-ns", domain="my-test-domain.com", provider="aws") + sut = SelfRegistrationStage(output_directory=None, config=config) stage_outputs = get_stage_outputs() result = sut.input_vars(stage_outputs) assert result["create_namespace"] == False assert result["namespace"] == "nebari-ns" + def test_chart_namespace(): - config = TestConfig(namespace = "nebari-ns", domain = "my-test-domain.com", provider="aws", self_registration = SelfRegistrationConfig(namespace = "self_registration-ns")) - sut = SelfRegistrationStage(output_directory = None, config = config) + config = TestConfig( + namespace="nebari-ns", + domain="my-test-domain.com", + provider="aws", + self_registration=SelfRegistrationConfig(namespace="self_registration-ns"), + ) + sut = SelfRegistrationStage(output_directory=None, config=config) stage_outputs = get_stage_outputs() result = sut.input_vars(stage_outputs) assert result["create_namespace"] == True assert result["namespace"] == "self_registration-ns" + def test_chart_overrides(): - config = TestConfig(namespace = "nebari-ns", domain = "my-test-domain.com", provider="aws", self_registration = SelfRegistrationConfig(values = { "foo": "bar" })) - sut = SelfRegistrationStage(output_directory = None, config = config) + config = TestConfig( + namespace="nebari-ns", + domain="my-test-domain.com", + provider="aws", + self_registration=SelfRegistrationConfig(values={"foo": "bar"}), + ) + sut = SelfRegistrationStage(output_directory=None, config=config) stage_outputs = get_stage_outputs() result = sut.input_vars(stage_outputs) - assert result["overrides"] == { "foo": "bar" } + assert result["overrides"] == {"foo": "bar"} + def get_stage_outputs(): return { - "stages/02-infrastructure": { - "cluster_oidc_issuer_url": { - "value": "https://test-oidc-url.com" - } - }, - "stages/04-kubernetes-ingress": { - "domain": "my-test-domain.com" - }, + "stages/02-infrastructure": {"cluster_oidc_issuer_url": {"value": "https://test-oidc-url.com"}}, + "stages/04-kubernetes-ingress": {"domain": "my-test-domain.com"}, "stages/05-kubernetes-keycloak": { "keycloak_credentials": { "value": { @@ -84,13 +119,9 @@ def get_stage_outputs(): "username": "testuser", "password": "testpassword", "realm": "testmasterrealm", - "client_id": "testmasterclientid" + "client_id": "testmasterclientid", } } }, - "stages/06-kubernetes-keycloak-configuration": { - "realm_id": { - "value": "test-realm" - } - } - } \ No newline at end of file + "stages/06-kubernetes-keycloak-configuration": {"realm_id": {"value": "test-realm"}}, + } From ab9f591d30d8eb937e6870c5dd77244dce837b42 Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Mon, 1 Jul 2024 09:39:37 -0500 Subject: [PATCH 3/9] Remove local selector. --- .../terraform/modules/self-registration/main.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf b/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf index 50fddb4..737b546 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf @@ -16,7 +16,6 @@ locals { affinity_selector_key = { aws = "eks.amazonaws.com/nodegroup" gcp = "cloud.google.com/gke-nodepool" - local = "node-role.kubernetes.io/control-plane" } } From 21b16c96da0b28cff680cb79637464a440e4e10e Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Mon, 1 Jul 2024 09:43:58 -0500 Subject: [PATCH 4/9] Change error message when adding user to one or more groups fails --- self-registration/app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/self-registration/app/main.py b/self-registration/app/main.py index d2496a3..8e9358c 100644 --- a/self-registration/app/main.py +++ b/self-registration/app/main.py @@ -190,7 +190,7 @@ async def validate_submission(request: Request, email: str = Form(...), coupon_c "index.html", get_template_context( request, - "User created but could not be assigned to JupyterLab group. Please contact support for assistance.", + "User created but could not be assigned to one or more groups. Please contact support for assistance.", ), ) else: From fc26f471c82efcdcb8de0fd603a9bd80d286bc74 Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Mon, 1 Jul 2024 10:53:19 -0500 Subject: [PATCH 5/9] Update readme with multi coupon configuration --- README.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 03caef8..3deea76 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,10 @@ The configuration of your self registration app can be customized in several way Configuration options include: - -- **account_expiration_days (optional)**: Days an account remains active after the user registers. Defaults to 7. Note that the calculated end date is saved in Keycloak user attribute `account_expiration_date` and can be manually overridden by a Keycloak administrator. -- **approved_domains (required)**: List of approved email domains that can register accounts using the self registration service. (supports names like `gmail.com` and wildcards such as `*.edu` or even `*`) -- **coupons (required)**: List of coupon codes that can be used by individuals during the self registration process. -- **registration_group (required)**: Keycloak group where all registering users will be added. This group can then be used to assign user properties such as available JupyterLab instance types, app sharing permissions, etc. +- **coupons (required)**: Map of coupon codes and their configuration that can be used by individuals during the self registration process. The coupon configuration options are: + - **account_expiration_days (optional)**: Days an account remains active after the user registers. Defaults to 7. Note that the calculated end date is saved in Keycloak user attribute `account_expiration_date` and can be manually overridden by a Keycloak administrator. + - **approved_domains (required)**: List of approved email domains that can register accounts using the self registration service. (supports names like `gmail.com` and wildcards such as `*.edu` or even `*`) + - **registration_groups (optional)**: List of Keycloak group where all registering users will be added. This group can then be used to assign user properties such as available JupyterLab instance types, app sharing permissions, etc. - **name (optional)**: Name for resources that this extension will deploy via Terraform and Helm. Defaults to `self-registration` - **namespace (optional)**: Kubernetes namespace for this service. Defaults to Nebari's default namespace. - **registration_message (optional)**: A custom message to display on the landing page `/registration` @@ -53,12 +52,17 @@ project_name: my-project self_registration: namespace: self-registration coupons: - - abcdefg - approved_domains: - - gmail.com - - '*.edu' - account_expiration_days: 30 - registration_group: test-group + abcdefg: + approved_domains: + - gmail.com + - '*.edu' + account_expiration_days: 30 + registration_groups: [test-group, developer] + hijklmn: + approved_domains: + - '*' + account_expiration_days: 7 + registration_groups: [admin] affinity: enabled: true selector: From d6db057e865d50cb03df80889804692af3311031 Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Wed, 3 Jul 2024 12:59:37 -0500 Subject: [PATCH 6/9] Avoid creating a keycloak client for each group. --- self-registration/app/main.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/self-registration/app/main.py b/self-registration/app/main.py index 8e9358c..53cb155 100644 --- a/self-registration/app/main.py +++ b/self-registration/app/main.py @@ -97,19 +97,19 @@ def generate_random_password(length=12): # Function to assign a user to a group def assign_user_to_groups(user, groups): - for group_name in groups: - try: - keycloak_admin = KeycloakAdmin( - server_url=config["keycloak"]["server_url"], - realm_name=config["keycloak"]["realm_name"], - client_id=config["keycloak"]["client_id"], - client_secret_key=config["keycloak"]["client_secret"], - user_realm_name=config["keycloak"]["realm_name"], - verify=True, - ) - except KeycloakConnectionError: - return False + try: + keycloak_admin = KeycloakAdmin( + server_url=config["keycloak"]["server_url"], + realm_name=config["keycloak"]["realm_name"], + client_id=config["keycloak"]["client_id"], + client_secret_key=config["keycloak"]["client_secret"], + user_realm_name=config["keycloak"]["realm_name"], + verify=True, + ) + except KeycloakConnectionError: + return False + for group_name in groups: # Get group try: group = keycloak_admin.get_group_by_path(group_name) From 761430cd39b0b87d52bffaa4a35b1802e5a91fa6 Mon Sep 17 00:00:00 2001 From: Marcelo Villa Date: Mon, 12 Aug 2024 14:47:59 -0500 Subject: [PATCH 7/9] Add note about changing configuration schema for the plugin --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3deea76..b6582fc 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ Configuration options include: > **NOTE:** The `registration_group` must have been created in the Nebari realm in Keycloak prior to deploying the extension. #### Example Nebari Config File + +> [!NOTE] +> The configuration options for the plugin were recently updated. Previously, `self_registration.coupons` accepted a list of coupon codes and there were shared options for all the specified coupons (e.g., `approved_domains`, `account_expiration_days`, etc...). Now, the field takes a map of coupon codes, where each coupon accepts individual configuration options (as outlined below). Please make sure to update the configuration values when updating to newer versions of the plugin after `0.0.12`. + ```yaml provider: aws namespace: dev From d47d2775f82c0bd5044fb767177b3607cdeb72b4 Mon Sep 17 00:00:00 2001 From: Ken Foster Date: Fri, 25 Oct 2024 11:12:49 -0400 Subject: [PATCH 8/9] Version and default image bumps --- src/nebari_plugin_self_registration/__about__.py | 2 +- .../terraform/modules/self-registration/chart/Chart.yaml | 4 ++-- .../terraform/modules/self-registration/chart/values.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nebari_plugin_self_registration/__about__.py b/src/nebari_plugin_self_registration/__about__.py index 311f216..6561790 100644 --- a/src/nebari_plugin_self_registration/__about__.py +++ b/src/nebari_plugin_self_registration/__about__.py @@ -1 +1 @@ -__version__ = "0.0.14" +__version__ = "0.0.15" diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml index fad4ed8..33c2aca 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.16 +version: 0.0.17 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.14" +appVersion: "0.0.15" diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml index f27019c..3c905ac 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml @@ -7,7 +7,7 @@ image: repository: quay.io/nebari/nebari-self-registration pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "20241023-1753" + tag: "20241025-1410" imagePullSecrets: [] nameOverride: "" From 26f6142ffae307588ad996f1a888b2222769811b Mon Sep 17 00:00:00 2001 From: Ken Foster Date: Fri, 25 Oct 2024 11:14:47 -0400 Subject: [PATCH 9/9] Update README version note --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6582fc..dd15cfb 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Configuration options include: #### Example Nebari Config File > [!NOTE] -> The configuration options for the plugin were recently updated. Previously, `self_registration.coupons` accepted a list of coupon codes and there were shared options for all the specified coupons (e.g., `approved_domains`, `account_expiration_days`, etc...). Now, the field takes a map of coupon codes, where each coupon accepts individual configuration options (as outlined below). Please make sure to update the configuration values when updating to newer versions of the plugin after `0.0.12`. +> The configuration options for the plugin were recently updated. Previously, `self_registration.coupons` accepted a list of coupon codes and there were shared options for all the specified coupons (e.g., `approved_domains`, `account_expiration_days`, etc...). Now, the field takes a map of coupon codes, where each coupon accepts individual configuration options (as outlined below). Please make sure to update the configuration values when updating to newer versions of the plugin after `0.0.14`. ```yaml provider: aws