193 lines
7.1 KiB
Python
193 lines
7.1 KiB
Python
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import Request, urlopen
|
|
|
|
import psycopg
|
|
from psycopg.rows import dict_row
|
|
|
|
from wiki_update_page import DEFAULT_WIKI_URL, WikiUpdateError, connect_info, find_auth_group, load_env, login
|
|
|
|
|
|
TEMP_PERMISSIONS = ["manage:system", "manage:assets", "read:assets", "write:assets"]
|
|
|
|
|
|
def normalize_json(value):
|
|
if value is None or isinstance(value, (dict, list)):
|
|
return value
|
|
return json.loads(value)
|
|
|
|
|
|
def get_json_cast(cur, column_name):
|
|
cur.execute(
|
|
"""
|
|
SELECT udt_name
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'groups' AND column_name = %s
|
|
""",
|
|
(column_name,),
|
|
)
|
|
row = cur.fetchone()
|
|
if not row:
|
|
raise WikiUpdateError(f"groups.{column_name} was not found")
|
|
return "jsonb" if row["udt_name"] == "jsonb" else "json"
|
|
|
|
|
|
def grant_temporary_permissions(cur, group):
|
|
permissions_cast = get_json_cast(cur, "permissions")
|
|
permissions = normalize_json(group["permissions"]) or []
|
|
added = []
|
|
for permission in TEMP_PERMISSIONS:
|
|
if permission not in permissions:
|
|
permissions.append(permission)
|
|
added.append(permission)
|
|
if added:
|
|
cur.execute(
|
|
f"UPDATE groups SET permissions = %s::{permissions_cast} WHERE id = %s",
|
|
(json.dumps(permissions), group["id"]),
|
|
)
|
|
print(f"temporary_permissions_added={','.join(added) if added else 'none'}")
|
|
return added
|
|
|
|
|
|
def remove_temporary_permissions(conninfo, group_id, added):
|
|
if not added:
|
|
return
|
|
with psycopg.connect(**conninfo, row_factory=dict_row) as conn:
|
|
with conn.cursor() as cur:
|
|
permissions_cast = get_json_cast(cur, "permissions")
|
|
cur.execute("SELECT permissions FROM groups WHERE id = %s", (group_id,))
|
|
row = cur.fetchone()
|
|
permissions = normalize_json(row["permissions"]) or []
|
|
permissions = [permission for permission in permissions if permission not in added]
|
|
cur.execute(
|
|
f"UPDATE groups SET permissions = %s::{permissions_cast} WHERE id = %s",
|
|
(json.dumps(permissions), group_id),
|
|
)
|
|
conn.commit()
|
|
print(f"temporary_permissions_removed={','.join(added)}")
|
|
|
|
|
|
def misplaced_assets(cur):
|
|
cur.execute(
|
|
"""
|
|
SELECT a.id, a.filename, a.hash, a."fileSize", f.slug AS folder_slug
|
|
FROM assets a
|
|
JOIN "assetFolders" f ON f.id = a."folderId"
|
|
WHERE f.slug = 'vulture'
|
|
AND (left(a.filename, 5) = 'echo_' OR left(a.filename, 11) = 'enterprise_')
|
|
ORDER BY a.filename, a.id
|
|
"""
|
|
)
|
|
return cur.fetchall()
|
|
|
|
|
|
def graphql(wiki_url, query, variables, token):
|
|
body = json.dumps({"query": query, "variables": variables}).encode("utf-8")
|
|
request = Request(
|
|
f"{wiki_url}/graphql",
|
|
data=body,
|
|
headers={"Content-Type": "application/json", "Accept": "application/json", "Authorization": f"Bearer {token}"},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urlopen(request, timeout=120) as response:
|
|
return json.loads(response.read().decode("utf-8"))
|
|
except HTTPError as exc:
|
|
return {"transportError": f"HTTP {exc.code}", "payload": exc.read().decode("utf-8", errors="replace")}
|
|
except URLError as exc:
|
|
return {"transportError": str(exc)}
|
|
|
|
|
|
def delete_asset(wiki_url, token, asset_id):
|
|
mutation = """
|
|
mutation DeleteAsset($id: Int!) {
|
|
assets {
|
|
deleteAsset(id: $id) {
|
|
responseResult { succeeded errorCode slug message }
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
payload = graphql(wiki_url, mutation, {"id": asset_id}, token)
|
|
if payload.get("errors"):
|
|
print(f"asset_delete id={asset_id} graphql_errors={json.dumps(payload['errors'], ensure_ascii=True)}")
|
|
return False
|
|
if payload.get("transportError"):
|
|
print(f"asset_delete id={asset_id} transport_error={payload['transportError']} payload={payload.get('payload', '')[:500]}")
|
|
return False
|
|
result = payload.get("data", {}).get("assets", {}).get("deleteAsset", {}).get("responseResult", {})
|
|
print(
|
|
"asset_delete id={id} succeeded={succeeded} errorCode={errorCode} slug={slug} message={message}".format(
|
|
id=asset_id,
|
|
succeeded=result.get("succeeded"),
|
|
errorCode=result.get("errorCode"),
|
|
slug=result.get("slug"),
|
|
message=result.get("message"),
|
|
)
|
|
)
|
|
return bool(result.get("succeeded"))
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Delete Echo/Enterprise assets that were mistakenly uploaded under the Vulture folder.")
|
|
parser.add_argument("--env-file", default=".env")
|
|
parser.add_argument("--wiki-url", default=DEFAULT_WIKI_URL)
|
|
parser.add_argument("--auth-group", default="automation")
|
|
parser.add_argument("--delete", action="store_true", help="Actually delete matching assets. Without this flag, only lists matches.")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
repo_root = Path.cwd()
|
|
env = load_env((repo_root / args.env_file).resolve())
|
|
conninfo = connect_info(env)
|
|
identities = []
|
|
for candidate in (env.get("wiki_useremail"), env.get("wiki_username")):
|
|
if candidate and candidate not in identities:
|
|
identities.append(candidate)
|
|
if not identities or not env.get("wiki_password"):
|
|
raise WikiUpdateError("Missing wiki_useremail/wiki_username or wiki_password in .env")
|
|
|
|
temporary_group_id = None
|
|
added_permissions = []
|
|
try:
|
|
with psycopg.connect(**conninfo, row_factory=dict_row) as conn:
|
|
with conn.cursor() as cur:
|
|
assets = misplaced_assets(cur)
|
|
print(f"misplaced_asset_count={len(assets)}")
|
|
for asset in assets:
|
|
print(f"misplaced_asset id={asset['id']} folder=/{asset['folder_slug']} filename={asset['filename']} size={asset['fileSize']}")
|
|
if not args.delete or not assets:
|
|
return 0
|
|
group = find_auth_group(cur, args.auth_group, [identity.lower() for identity in identities])
|
|
added_permissions = grant_temporary_permissions(cur, group)
|
|
temporary_group_id = group["id"]
|
|
conn.commit()
|
|
|
|
token = login(args.wiki_url.rstrip("/"), identities, env["wiki_password"])
|
|
ok = True
|
|
for asset in assets:
|
|
ok = delete_asset(args.wiki_url.rstrip("/"), token, asset["id"]) and ok
|
|
if not ok:
|
|
return 1
|
|
|
|
with psycopg.connect(**conninfo, row_factory=dict_row) as conn:
|
|
with conn.cursor() as cur:
|
|
remaining = misplaced_assets(cur)
|
|
print(f"misplaced_asset_remaining={len(remaining)}")
|
|
return 0 if not remaining else 1
|
|
finally:
|
|
if temporary_group_id is not None:
|
|
remove_temporary_permissions(conninfo, temporary_group_id, added_permissions)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except WikiUpdateError as exc:
|
|
print(f"wiki_cleanup_error={exc}")
|
|
sys.exit(2) |