Bug description

When i run this code,

import requests
import os
import json

# --- REQUIRED CONFIGURATION ---
SUPERSET_URL = os.getenv("SUPERSET_URL", "http://localhost:8088")
ADMIN_USERNAME = os.getenv("SUPERSET_USER", "admin")
ADMIN_PASSWORD = os.getenv("SUPERSET_PASSWORD", "admin")

# --- PROGRAM STUDY CODE MAPPING (KAPRODI) ---
# REQUIRED: Complete this dictionary with your data.
# Format: "short_program_name_from_role": numeric_program_code
KAPRODI_CODE_MAPPING = {
    "if": 135,
    "mesin": 131,
    # "elektro": 133,
    # Add all your kaprodi (program study) mappings here
}

def main():
    """Main function to run the automation process with CREATE or UPDATE logic."""

    # Use a session object to maintain login cookies, which are needed for CSRF
    with requests.Session() as session:
        # 1. Authenticate and get CSRF token
        print("1. Authenticating to Superset...")
        try:
            # Login
            login_data = {"username": ADMIN_USERNAME, "password": ADMIN_PASSWORD, "provider": "db"}
            r_login = session.post(f"{SUPERSET_URL}/api/v1/security/login", json=login_data)
            r_login.raise_for_status()
            access_token = r_login.json()["access_token"]

            # Set headers for the entire session
            session.headers.update({
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json"
            })
            print("   -> Authentication successful.")

            # Get CSRF token
            print("   -> Fetching CSRF token...")
            r_csrf = session.get(f"{SUPERSET_URL}/api/v1/security/csrf_token/")
            r_csrf.raise_for_status()
            csrf_token = r_csrf.json()['result']

            # Add CSRF token to headers for all future state-changing requests (POST, PUT, DELETE)
            session.headers['X-CSRFToken'] = csrf_token
            print("   -> CSRF token obtained.")

        except requests.exceptions.RequestException as e:
            print(f"   -> FAILED during login/CSRF process: {e}")
            return

        # 2. Fetch all necessary data
        print("\n2. Fetching data from Superset...")
        params = {"q": json.dumps({"page_size": -1})}

        try:
            r_datasets = session.get(f"{SUPERSET_URL}/api/v1/dataset/", params=params)
            r_datasets.raise_for_status()
            all_dataset_ids = [ds['id'] for ds in r_datasets.json()['result']]

            r_roles = session.get(f"{SUPERSET_URL}/api/v1/security/roles/", params=params)
            r_roles.raise_for_status()
            roles_map = {role['name']: role['id'] for role in r_roles.json()['result']}

            # IMPORTANT CHANGE: Store the NAME and ID of existing rules in a dictionary/map
            r_rls = session.get(f"{SUPERSET_URL}/api/v1/rowlevelsecurity/", params=params)
            r_rls.raise_for_status()
            existing_rules_map = {rule['name']: rule['id'] for rule in r_rls.json()['result']}

            print(f"   -> Found {len(all_dataset_ids)} datasets, {len(roles_map)} roles, and {len(existing_rules_map)} existing RLS rules.")
        except requests.exceptions.RequestException as e:
            print(f"   -> FAILED to fetch data: {e}")
            return

        if not all_dataset_ids:
            print("\nWARNING: No datasets found. No rules will be created or updated.")
            return

        print("\n3. Processing roles and creating or updating RLS rules...")

        for role_name, role_id in roles_map.items():
            rule_name = None
            clause = None

            # Logic to determine rule_name and clause remains the same
            if role_name.lower().startswith('dekan ') and role_name.lower() != 'dekan':
                fakultas_code = role_name.split(' ', 1)[1].upper()
                rule_name = f"Rule Dekan {fakultas_code}"
                clause = f"kd_fak = '{fakultas_code}'"

            elif role_name.lower().startswith('kaprodi ') and role_name.lower() != 'kaprodi':
                prodi_short_name = role_name.split(' ', 1)[1].lower()
                prodi_code = KAPRODI_CODE_MAPPING.get(prodi_short_name)
                if prodi_code:
                    rule_name = f"Rule Kaprodi {prodi_short_name.upper()}"
                    clause = f"no_ps = {prodi_code}"
                else:
                    if prodi_short_name: # Only print warning if there is a name to warn about
                        print(f"\n- WARNING: No code mapping found for '{prodi_short_name}'. Skipping role '{role_name}'.")
                    continue

            # --- NEW LOGIC: CREATE OR UPDATE ---
            if rule_name:
                print(f"\n- Processing rule for role: '{role_name}'")

                # This payload will be used for both creating (POST) and updating (PUT)
                payload = {
                    "name": rule_name,
                    "filter_type": "Regular",
                    "clause": clause,
                    "roles": [role_id],
                    "tables": all_dataset_ids,
                    "description": "Automatically created or updated by Python script."
                }

                if rule_name in existing_rules_map:
                    # --- IF RULE EXISTS: PERFORM UPDATE (PUT) ---
                    rule_id_to_update = existing_rules_map[rule_name]
                    print(f"  -> Rule '{rule_name}' exists (ID: {rule_id_to_update}). Updating...")
                    try:
                        r_update = session.put(f"{SUPERSET_URL}/api/v1/rowlevelsecurity/{rule_id_to_update}", json=payload)
                        if r_update.status_code == 200:
                            print(f"  -> SUCCESS: Rule updated.")
                        else:
                            print(f"  -> FAILED to update. Status: {r_update.status_code}, Message: {r_update.text}")
                    except requests.exceptions.RequestException as e:
                        print(f"  -> FAILED due to an exception: {e}")
                else:
                    # --- IF RULE DOES NOT EXIST: PERFORM CREATE (POST) ---
                    print(f"  -> Rule '{rule_name}' does not exist. Creating...")
                    try:
                        r_create = session.post(f"{SUPERSET_URL}/api/v1/rowlevelsecurity/", json=payload)
                        if r_create.status_code == 201:
                            print(f"  -> SUCCESS: Rule created.")
                            # Add the new rule to the map so we don't try to re-create it in the same session
                            new_rule_id = r_create.json().get('id')
                            if new_rule_id:
                                existing_rules_map[rule_name] = new_rule_id
                        else:
                            print(f"  -> FAILED to create. Status: {r_create.status_code}, Message: {r_create.text}")
                    except requests.exceptions.RequestException as e:
                        print(f"  -> FAILED due to an exception: {e}")

    print("\n4. Process finished.")

if __name__ == "__main__":
    main()

i got an error message when try to update rules.

- Processing rule for role: 'Kaprodi Mesin'
  -> Rule 'Rule Kaprodi MESIN' exists (ID: 12). Updating...
  -> FAILED to update. Status: 201, Message: {
  "id": 12,
  "result": {
    "clause": "no_ps = 131",
    "description": "Automatically created or updated by Python script.",
    "filter_type": "Regular",
    "name": "Rule Kaprodi MESIN",
    "roles": [
      9
    ],
    "tables": [
      25,
      20,
      11,
      17,
      12,
      10,
      18,
      2,
      15,
      13,
      21,
      5,
      19,
      24,
      8,
      6,
      16,
      4,
      23,
      1,
      22,
      3,
      14,
      9,
      7
    ]
  }
}

In https://superset.apache.org/docs/api, there is not response with code 201 at method put endpoint /api/v1/rowlevelsecurity/{pk}.

Anyone can help me?

Screenshots/recordings

No response

Superset version

5.0.0

Python version

3.11

Node version

18 or greater

Browser

Chrome

Additional context

No response

Checklist

  • [x] I have searched Superset docs and Slack and didn't find a solution to my problem.
  • [x] I have searched the GitHub issue tracker and didn't find a similar bug report.
  • [x] I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

Comment From: dosubot[bot]

This is a bug in Superset 5.0.0: the PUT /api/v1/rowlevelsecurity/{pk} endpoint returns a 201 status code on update, even though the API docs and REST standards say it should return 200. The code explicitly returns 201 on successful update, which is nonstandard and not intended behavior (see source code). There are no existing issues or fixes for this yet.

As a workaround, you can treat both 200 and 201 as success for this endpoint in your automation scripts. This bug could affect other endpoints that use the same generic API base classes, so watch for similar patterns elsewhere.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

Comment From: msyavuz

Nice catch! You are right that 201 should be used with resource creation only. Opened a pr to fix it.