This is an automated email from the ASF dual-hosted git repository.

EnxDev pushed a commit to branch enxdev/chat-prototype
in repository https://gitbox.apache.org/repos/asf/superset.git

commit c47fb16ebe3fc4f50087c1a6635ff884c00af6d7
Author: Enzo Martellucci <[email protected]>
AuthorDate: Tue May 26 16:11:00 2026 +0200

    feat(extensions): backend settings persistence and admin-only permissions
    
    Adds ExtensionSettings and ExtensionEnabled models with migration.
    GET /api/v1/extensions/settings is public; PUT is restricted to Admin
    role via security_manager.is_admin(). Uses dialect-aware ON CONFLICT DO
    UPDATE upserts and @transaction() for safe concurrent writes.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 superset/extensions/api.py                         |  54 ++++++++++-
 superset/extensions/settings.py                    | 108 +++++++++++++++++++++
 ...25_00-00_b2c3d4e5f6a7_add_extension_settings.py |  47 +++++++++
 superset/models/core.py                            |  16 +++
 4 files changed, 224 insertions(+), 1 deletion(-)

diff --git a/superset/extensions/api.py b/superset/extensions/api.py
index b1b5734979e..d116c6ca70e 100644
--- a/superset/extensions/api.py
+++ b/superset/extensions/api.py
@@ -18,10 +18,15 @@ import mimetypes
 from io import BytesIO
 from typing import Any
 
-from flask import send_file
+from flask import request, send_file
 from flask.wrappers import Response
 from flask_appbuilder.api import BaseApi, expose, protect, safe
 
+from superset.extensions import security_manager
+from superset.extensions.settings import (
+    get_extension_settings,
+    update_extension_settings,
+)
 from superset.extensions.utils import (
     build_extension_data,
     get_extensions,
@@ -167,6 +172,53 @@ class ExtensionsRestApi(BaseApi):
         extension_data = build_extension_data(extension)
         return self.response(200, result=extension_data)
 
+    @protect()
+    @safe
+    @expose("/settings", methods=("GET",))
+    def get_settings(self, **kwargs: Any) -> Response:
+        """Get global extension admin settings.
+        ---
+        get:
+          summary: Get extension admin settings (active chatbot, enabled 
flags).
+          responses:
+            200:
+              description: Extension settings
+        """
+        return self.response(200, result=get_extension_settings())
+
+    @protect()
+    @safe
+    @expose("/settings", methods=("PUT",))
+    def put_settings(self, **kwargs: Any) -> Response:
+        """Update global extension admin settings.
+        ---
+        put:
+          summary: Update extension admin settings.
+          requestBody:
+            content:
+              application/json:
+                schema:
+                  type: object
+                  properties:
+                    active_chatbot_id:
+                      type: string
+                      nullable: true
+                    enabled:
+                      type: object
+                      additionalProperties:
+                        type: boolean
+          responses:
+            200:
+              description: Updated settings
+            403:
+              $ref: '#/components/responses/403'
+        """
+        if not security_manager.is_admin():
+            return self.response(403, message="Admin access required.")
+        body = request.get_json(silent=True) or {}
+        result = update_extension_settings(body)
+        return self.response(200, result=result)
+
     @protect()
     @safe
     @expose("/<publisher>/<name>/<file>", methods=("GET",))
diff --git a/superset/extensions/settings.py b/superset/extensions/settings.py
new file mode 100644
index 00000000000..cd04a424dea
--- /dev/null
+++ b/superset/extensions/settings.py
@@ -0,0 +1,108 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Admin settings persistence for extensions (active chatbot, 
enable/disable)."""
+
+from typing import Any
+
+from sqlalchemy.dialects.postgresql import insert as pg_insert
+from sqlalchemy.dialects.sqlite import insert as sqlite_insert
+
+from superset import db
+from superset.models.core import ExtensionEnabled, ExtensionSettings
+from superset.utils.decorators import transaction
+
+_SETTINGS_ROW_ID = 1
+
+
+def get_extension_settings() -> dict[str, Any]:
+    row = db.session.get(ExtensionSettings, _SETTINGS_ROW_ID)
+    enabled_rows = db.session.query(ExtensionEnabled).all()
+    return {
+        "active_chatbot_id": row.active_chatbot_id if row else None,
+        "enabled": {r.extension_id: r.enabled for r in enabled_rows},
+    }
+
+
+def _upsert_settings_row(
+    active_chatbot_id: str | None,
+) -> None:
+    """Upsert the singleton settings row without a read-then-insert race."""
+    bind = db.session.get_bind()
+    dialect = bind.dialect.name
+    if dialect == "postgresql":
+        stmt = (
+            pg_insert(ExtensionSettings)
+            .values(id=_SETTINGS_ROW_ID, active_chatbot_id=active_chatbot_id)
+            .on_conflict_do_update(
+                index_elements=["id"],
+                set_={"active_chatbot_id": active_chatbot_id},
+            )
+        )
+        db.session.execute(stmt)
+    else:
+        stmt = (
+            sqlite_insert(ExtensionSettings)
+            .values(id=_SETTINGS_ROW_ID, active_chatbot_id=active_chatbot_id)
+            .on_conflict_do_update(
+                index_elements=["id"],
+                set_={"active_chatbot_id": active_chatbot_id},
+            )
+        )
+        db.session.execute(stmt)
+
+
+def _upsert_enabled_flag(extension_id: str, enabled: bool) -> None:
+    """Upsert a per-extension enabled flag without a read-then-insert race."""
+    bind = db.session.get_bind()
+    dialect = bind.dialect.name
+    if dialect == "postgresql":
+        stmt = (
+            pg_insert(ExtensionEnabled)
+            .values(extension_id=extension_id, enabled=enabled)
+            .on_conflict_do_update(
+                index_elements=["extension_id"],
+                set_={"enabled": enabled},
+            )
+        )
+        db.session.execute(stmt)
+    else:
+        stmt = (
+            sqlite_insert(ExtensionEnabled)
+            .values(extension_id=extension_id, enabled=enabled)
+            .on_conflict_do_update(
+                index_elements=["extension_id"],
+                set_={"enabled": enabled},
+            )
+        )
+        db.session.execute(stmt)
+
+
+@transaction()
+def update_extension_settings(body: dict[str, Any]) -> dict[str, Any]:
+    if "active_chatbot_id" in body:
+        value = body["active_chatbot_id"]
+        active_chatbot_id = str(value) if isinstance(value, str) and value 
else None
+        _upsert_settings_row(active_chatbot_id)
+
+    if "enabled" in body and isinstance(body["enabled"], dict):
+        for extension_id, enabled in body["enabled"].items():
+            if not isinstance(enabled, bool):
+                continue
+            _upsert_enabled_flag(extension_id, enabled)
+
+    return get_extension_settings()
diff --git 
a/superset/migrations/versions/2026-05-25_00-00_b2c3d4e5f6a7_add_extension_settings.py
 
b/superset/migrations/versions/2026-05-25_00-00_b2c3d4e5f6a7_add_extension_settings.py
new file mode 100644
index 00000000000..9773a5104aa
--- /dev/null
+++ 
b/superset/migrations/versions/2026-05-25_00-00_b2c3d4e5f6a7_add_extension_settings.py
@@ -0,0 +1,47 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Add extension_settings table for chatbot admin selection and enable/disable.
+
+Revision ID: b2c3d4e5f6a7
+Revises: a1b2c3d4e5f6
+Create Date: 2026-05-25 00:00:00.000000
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+revision = "b2c3d4e5f6a7"
+down_revision = "a1b2c3d4e5f6"
+
+
+def upgrade() -> None:
+    op.create_table(
+        "extension_settings",
+        sa.Column("id", sa.Integer(), primary_key=True),
+        sa.Column("active_chatbot_id", sa.String(250), nullable=True),
+    )
+    op.create_table(
+        "extension_enabled",
+        sa.Column("extension_id", sa.String(250), primary_key=True),
+        sa.Column("enabled", sa.Boolean(), nullable=False, server_default="1"),
+    )
+
+
+def downgrade() -> None:
+    op.drop_table("extension_enabled")
+    op.drop_table("extension_settings")
diff --git a/superset/models/core.py b/superset/models/core.py
index 1f99630ab3d..35a36f588d5 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -108,6 +108,22 @@ class KeyValue(Model):  # pylint: 
disable=too-few-public-methods
     value = Column(utils.MediumText(), nullable=False)
 
 
+class ExtensionSettings(Model):  # pylint: disable=too-few-public-methods
+    """Global admin settings for extensions (singleton row, id=1)."""
+
+    __tablename__ = "extension_settings"
+    id = Column(Integer, primary_key=True)
+    active_chatbot_id = Column(String(250), nullable=True)
+
+
+class ExtensionEnabled(Model):  # pylint: disable=too-few-public-methods
+    """Per-extension enable/disable flag."""
+
+    __tablename__ = "extension_enabled"
+    extension_id = Column(String(250), primary_key=True)
+    enabled = Column(Boolean, nullable=False, default=True)
+
+
 class CssTemplate(AuditMixinNullable, UUIDMixin, Model):
     """CSS templates for dashboards"""
 

Reply via email to