From 04140d671ed6fa2d45541caeefb50e6fcb37ffd9 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Mon, 22 Dec 2025 21:03:10 +0100 Subject: [PATCH 1/3] feat(helm): add support for multi-user BasicAuth mode Adds multi-user-basic authentication mode to the helm chart alongside existing basic (single-user) and oauth modes. Multi-user BasicAuth mode enables: - Pass-through authentication (credentials in request headers) - Optional background operations using app passwords via Astrolabe - Optional OAuth client credentials (uses DCR if not provided) - Token encryption and persistent storage for background sync Changes: - values.yaml: Add auth.multiUserBasic configuration section - deployment.yaml: Add ENABLE_MULTI_USER_BASIC_AUTH and related env vars - secret.yaml: Add secret template for token encryption key and OAuth credentials - pvc.yaml: Add PVC template for token database persistence - _helpers.tpl: Add helper functions for secret/PVC names Tested with: helm template --set auth.mode=multi-user-basic \ --set auth.multiUserBasic.enableOfflineAccess=true \ --set auth.multiUserBasic.tokenEncryptionKey=... \ --set vectorSync.enabled=true Related: Multi-user deployment support (ADR-020) --- .../templates/_helpers.tpl | 22 ++++++++ .../templates/deployment.yaml | 46 ++++++++++++++++- .../nextcloud-mcp-server/templates/pvc.yaml | 18 +++++++ .../templates/secret.yaml | 18 +++++++ charts/nextcloud-mcp-server/values.yaml | 50 +++++++++++++++++-- 5 files changed, 149 insertions(+), 5 deletions(-) diff --git a/charts/nextcloud-mcp-server/templates/_helpers.tpl b/charts/nextcloud-mcp-server/templates/_helpers.tpl index d6656b3..1dcf194 100644 --- a/charts/nextcloud-mcp-server/templates/_helpers.tpl +++ b/charts/nextcloud-mcp-server/templates/_helpers.tpl @@ -72,6 +72,28 @@ Create the name of the secret to use for basic auth {{- end }} {{- end }} +{{/* +Create the name of the secret to use for multi-user basic auth +*/}} +{{- define "nextcloud-mcp-server.multiUserBasicSecretName" -}} +{{- if .Values.auth.multiUserBasic.existingSecret }} +{{- .Values.auth.multiUserBasic.existingSecret }} +{{- else }} +{{- include "nextcloud-mcp-server.fullname" . }}-multi-user-basic +{{- end }} +{{- end }} + +{{/* +Create the name of the PVC to use for multi-user basic token storage +*/}} +{{- define "nextcloud-mcp-server.multiUserBasicPvcName" -}} +{{- if .Values.auth.multiUserBasic.persistence.existingClaim }} +{{- .Values.auth.multiUserBasic.persistence.existingClaim }} +{{- else }} +{{- include "nextcloud-mcp-server.fullname" . }}-token-storage +{{- end }} +{{- end }} + {{/* Create the name of the secret to use for OAuth */}} diff --git a/charts/nextcloud-mcp-server/templates/deployment.yaml b/charts/nextcloud-mcp-server/templates/deployment.yaml index 6471ebb..0d9cb03 100644 --- a/charts/nextcloud-mcp-server/templates/deployment.yaml +++ b/charts/nextcloud-mcp-server/templates/deployment.yaml @@ -68,7 +68,7 @@ spec: - name: NEXTCLOUD_HOST value: {{ .Values.nextcloud.host | quote }} {{- if eq .Values.auth.mode "basic" }} - # Basic auth mode + # Basic auth mode (single-user) - name: NEXTCLOUD_USERNAME valueFrom: secretKeyRef: @@ -79,6 +79,41 @@ spec: secretKeyRef: name: {{ include "nextcloud-mcp-server.basicAuthSecretName" . }} key: {{ .Values.auth.basic.passwordKey }} + {{- else if eq .Values.auth.mode "multi-user-basic" }} + # Multi-user BasicAuth mode (pass-through) + - name: ENABLE_MULTI_USER_BASIC_AUTH + value: "true" + - name: NEXTCLOUD_MCP_SERVER_URL + value: {{ include "nextcloud-mcp-server.mcpServerUrl" . | quote }} + - name: NEXTCLOUD_PUBLIC_ISSUER_URL + value: {{ include "nextcloud-mcp-server.publicIssuerUrl" . | quote }} + {{- if .Values.auth.multiUserBasic.enableOfflineAccess }} + # Background operations with app passwords + - name: ENABLE_OFFLINE_ACCESS + value: "true" + - name: TOKEN_STORAGE_DB + value: {{ .Values.auth.multiUserBasic.tokenStorageDb | quote }} + - name: TOKEN_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ include "nextcloud-mcp-server.multiUserBasicSecretName" . }} + key: {{ .Values.auth.multiUserBasic.tokenEncryptionKeyKey }} + - name: NEXTCLOUD_OIDC_SCOPES + value: {{ .Values.auth.multiUserBasic.scopes | quote }} + {{- if or .Values.auth.multiUserBasic.clientId .Values.auth.multiUserBasic.existingSecret }} + # Static OAuth credentials (optional - uses DCR if not provided) + - name: NEXTCLOUD_OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ include "nextcloud-mcp-server.multiUserBasicSecretName" . }} + key: {{ .Values.auth.multiUserBasic.clientIdKey }} + - name: NEXTCLOUD_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "nextcloud-mcp-server.multiUserBasicSecretName" . }} + key: {{ .Values.auth.multiUserBasic.clientSecretKey }} + {{- end }} + {{- end }} {{- else if eq .Values.auth.mode "oauth" }} # OAuth mode - name: NEXTCLOUD_MCP_SERVER_URL @@ -251,6 +286,10 @@ spec: - name: oauth-storage mountPath: /app/.oauth {{- end }} + {{- if and (eq .Values.auth.mode "multi-user-basic") .Values.auth.multiUserBasic.enableOfflineAccess .Values.auth.multiUserBasic.persistence.enabled }} + - name: token-storage + mountPath: /app/data + {{- end }} {{- if and (eq .Values.qdrant.mode "persistent") .Values.qdrant.localPersistence.enabled }} - name: qdrant-data mountPath: /app/data @@ -266,6 +305,11 @@ spec: persistentVolumeClaim: claimName: {{ include "nextcloud-mcp-server.oauthPvcName" . }} {{- end }} + {{- if and (eq .Values.auth.mode "multi-user-basic") .Values.auth.multiUserBasic.enableOfflineAccess .Values.auth.multiUserBasic.persistence.enabled }} + - name: token-storage + persistentVolumeClaim: + claimName: {{ include "nextcloud-mcp-server.multiUserBasicPvcName" . }} + {{- end }} {{- if and (eq .Values.qdrant.mode "persistent") .Values.qdrant.localPersistence.enabled }} - name: qdrant-data persistentVolumeClaim: diff --git a/charts/nextcloud-mcp-server/templates/pvc.yaml b/charts/nextcloud-mcp-server/templates/pvc.yaml index fee7580..f65a100 100644 --- a/charts/nextcloud-mcp-server/templates/pvc.yaml +++ b/charts/nextcloud-mcp-server/templates/pvc.yaml @@ -16,6 +16,24 @@ spec: storage: {{ .Values.auth.oauth.persistence.size }} {{- end }} --- +{{- if and (eq .Values.auth.mode "multi-user-basic") .Values.auth.multiUserBasic.enableOfflineAccess .Values.auth.multiUserBasic.persistence.enabled (not .Values.auth.multiUserBasic.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nextcloud-mcp-server.fullname" . }}-token-storage + labels: + {{- include "nextcloud-mcp-server.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.auth.multiUserBasic.persistence.accessMode }} + {{- if .Values.auth.multiUserBasic.persistence.storageClass }} + storageClassName: {{ .Values.auth.multiUserBasic.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.auth.multiUserBasic.persistence.size }} +{{- end }} +--- {{- if and (eq .Values.qdrant.mode "persistent") .Values.qdrant.localPersistence.enabled (not .Values.qdrant.localPersistence.existingClaim) }} apiVersion: v1 kind: PersistentVolumeClaim diff --git a/charts/nextcloud-mcp-server/templates/secret.yaml b/charts/nextcloud-mcp-server/templates/secret.yaml index 2039fa3..eed9b81 100644 --- a/charts/nextcloud-mcp-server/templates/secret.yaml +++ b/charts/nextcloud-mcp-server/templates/secret.yaml @@ -13,6 +13,24 @@ data: {{- end }} {{- end }} --- +{{- if eq .Values.auth.mode "multi-user-basic" }} +{{- if and .Values.auth.multiUserBasic.enableOfflineAccess (not .Values.auth.multiUserBasic.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nextcloud-mcp-server.fullname" . }}-multi-user-basic + labels: + {{- include "nextcloud-mcp-server.labels" . | nindent 4 }} +type: Opaque +data: + {{ .Values.auth.multiUserBasic.tokenEncryptionKeyKey }}: {{ .Values.auth.multiUserBasic.tokenEncryptionKey | b64enc | quote }} + {{- if .Values.auth.multiUserBasic.clientId }} + {{ .Values.auth.multiUserBasic.clientIdKey }}: {{ .Values.auth.multiUserBasic.clientId | b64enc | quote }} + {{ .Values.auth.multiUserBasic.clientSecretKey }}: {{ .Values.auth.multiUserBasic.clientSecret | b64enc | quote }} + {{- end }} +{{- end }} +{{- end }} +--- {{- if eq .Values.auth.mode "oauth" }} {{- if and .Values.auth.oauth.clientId (not .Values.auth.oauth.existingSecret) }} apiVersion: v1 diff --git a/charts/nextcloud-mcp-server/values.yaml b/charts/nextcloud-mcp-server/values.yaml index a14a65b..b267507 100644 --- a/charts/nextcloud-mcp-server/values.yaml +++ b/charts/nextcloud-mcp-server/values.yaml @@ -33,14 +33,15 @@ nextcloud: publicIssuerUrl: "" # Authentication configuration -# Choose either basic auth OR oauth (not both) +# Choose one mode: "basic", "multi-user-basic", or "oauth" auth: - # Authentication mode: "basic" or "oauth" - # basic: Uses username/password (recommended for most users) + # Authentication mode: "basic", "multi-user-basic", or "oauth" + # basic: Single-user with username/password (recommended for personal use) + # multi-user-basic: Multi-user with BasicAuth pass-through (credentials in request headers) # oauth: Uses OAuth2/OIDC (experimental, requires patches) mode: basic - # Basic authentication settings + # Basic authentication settings (single-user mode) basic: # Nextcloud username (ignored if existingSecret is set) username: "" @@ -58,6 +59,47 @@ auth: usernameKey: "username" passwordKey: "password" + # Multi-user BasicAuth settings (pass-through mode) + # Users provide credentials in request headers (Authorization: Basic ...) + # Server optionally stores app passwords for background operations + multiUserBasic: + # Enable offline access (background operations using app passwords via Astrolabe) + # When enabled, requires token encryption key and OAuth client credentials + enableOfflineAccess: false + # Token encryption key (required if enableOfflineAccess: true, ignored if existingSecret is set) + # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" + tokenEncryptionKey: "" + # Token storage database path + tokenStorageDb: "/app/data/tokens.db" + # OAuth client credentials (optional - uses Dynamic Client Registration if not provided) + # Only needed if enableOfflineAccess: true + clientId: "" + clientSecret: "" + # OAuth scopes to request (space-separated) + scopes: "openid profile email offline_access notes:read notes:write calendar:read calendar:write contacts:read contacts:write cookbook:read cookbook:write deck:read deck:write tables:read tables:write files:read files:write sharing:read sharing:write todo:read todo:write" + # Use existing secret for multi-user basic auth credentials + # If set, tokenEncryptionKey, clientId, and clientSecret above are ignored + # Secret should contain keys specified in the *Key fields below + # Example: + # kubectl create secret generic my-multiuser-creds \ + # --from-literal=token_encryption_key=ESF1BvEQ... \ + # --from-literal=client_id=my-client-id \ + # --from-literal=client_secret=my-client-secret + existingSecret: "" + # Keys in the existing secret + tokenEncryptionKeyKey: "token_encryption_key" + clientIdKey: "client_id" + clientSecretKey: "client_secret" + # Persistent storage for token database + persistence: + enabled: true + # Storage class (leave empty for default) + storageClass: "" + accessMode: ReadWriteOnce + size: 100Mi + # Use existing PVC + existingClaim: "" + # OAuth2/OIDC settings (experimental) oauth: # OAuth token type: "jwt" or "opaque" From 9b5c6779e9b7bb43cb3ba12ef7c889759d21b2b3 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Mon, 22 Dec 2025 21:17:14 +0100 Subject: [PATCH 2/3] fix(helm): include MCP server version bumps in changelog pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates helm chart commitizen config to recognize MCP server version bump commits (which update appVersion in Chart.yaml) as valid triggers for helm chart version bumps. Problem: - When MCP server version bumps, it updates Chart.yaml appVersion - Helm chart commitizen only matched "(helm)" scoped commits - Result: appVersion updated but chart version not bumped Solution: - Extended changelog_pattern to include "bump: version X → Y" commits - Now helm chart version will bump when either: 1. Commits with (helm) scope are made, OR 2. MCP server version bumps (updating appVersion) This ensures chart version stays in sync with appVersion updates. --- charts/nextcloud-mcp-server/.cz.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/charts/nextcloud-mcp-server/.cz.toml b/charts/nextcloud-mcp-server/.cz.toml index f9ea19f..409ed6d 100644 --- a/charts/nextcloud-mcp-server/.cz.toml +++ b/charts/nextcloud-mcp-server/.cz.toml @@ -18,7 +18,8 @@ ignored_tag_formats = [ ] # Filter commits by scope +# Includes helm-scoped commits AND MCP server version bumps (which update appVersion) [tool.commitizen.customize] -changelog_pattern = "^(feat|fix|docs|refactor|perf|test|build|ci|chore)\\(helm\\)(!)?:" +changelog_pattern = "^((feat|fix|docs|refactor|perf|test|build|ci|chore)\\(helm\\)(!)?:|bump: version.*→.*)" schema_pattern = "^(feat|fix|docs|refactor|perf|test|build|ci|chore)\\(helm\\)(!)?:\\s.+" message_template = "{{change_type}}(helm): {{message}}" From ea96a586785fa431a5dc27a7c212d1226ac7d519 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Mon, 22 Dec 2025 21:34:40 +0100 Subject: [PATCH 3/3] fix(helm): address PR #447 reviewer feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fix: - deployment.yaml: Only reference OAuth credentials when clientId is set - Fixes pod failure when using existingSecret without static OAuth creds - Aligns deployment behavior with secret template logic Previously, the deployment referenced OAuth credentials when either clientId OR existingSecret was set. However, the secret template only includes OAuth credentials when clientId is explicitly provided. This caused pod failures when users provided an existingSecret for offline access without static OAuth credentials (intending to use DCR). The fix ensures OAuth env vars are only referenced when clientId is set, matching the OAuth mode pattern and allowing DCR to work correctly with existingSecret configurations. Minor improvements: - values.yaml: Clarify OAuth credentials are optional (uses DCR if not provided) Testing verified all scenarios: ✅ Pass-through only (no offline access): No secrets/PVCs/OAuth vars ✅ Offline + DCR (no clientId): Secret with encryption key only, no OAuth vars ✅ Offline + static OAuth: Secret with all keys, OAuth vars present ✅ existingSecret without clientId: No auto secret, no OAuth vars (FIXED) Resolves reviewer feedback from PR #447 --- charts/nextcloud-mcp-server/templates/deployment.yaml | 2 +- charts/nextcloud-mcp-server/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/nextcloud-mcp-server/templates/deployment.yaml b/charts/nextcloud-mcp-server/templates/deployment.yaml index 0d9cb03..705b7f1 100644 --- a/charts/nextcloud-mcp-server/templates/deployment.yaml +++ b/charts/nextcloud-mcp-server/templates/deployment.yaml @@ -100,7 +100,7 @@ spec: key: {{ .Values.auth.multiUserBasic.tokenEncryptionKeyKey }} - name: NEXTCLOUD_OIDC_SCOPES value: {{ .Values.auth.multiUserBasic.scopes | quote }} - {{- if or .Values.auth.multiUserBasic.clientId .Values.auth.multiUserBasic.existingSecret }} + {{- if .Values.auth.multiUserBasic.clientId }} # Static OAuth credentials (optional - uses DCR if not provided) - name: NEXTCLOUD_OIDC_CLIENT_ID valueFrom: diff --git a/charts/nextcloud-mcp-server/values.yaml b/charts/nextcloud-mcp-server/values.yaml index b267507..3911c93 100644 --- a/charts/nextcloud-mcp-server/values.yaml +++ b/charts/nextcloud-mcp-server/values.yaml @@ -64,7 +64,7 @@ auth: # Server optionally stores app passwords for background operations multiUserBasic: # Enable offline access (background operations using app passwords via Astrolabe) - # When enabled, requires token encryption key and OAuth client credentials + # When enabled, requires token encryption key. OAuth client credentials are optional (uses DCR if not provided) enableOfflineAccess: false # Token encryption key (required if enableOfflineAccess: true, ignored if existingSecret is set) # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"