mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
- add admin role CRUD and membership management - add non-protobuf control role operations Signed-off-by: 21pages <sunboeasy@gmail.com>
418 lines
13 KiB
Python
Executable File
418 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import json
|
|
|
|
import requests
|
|
|
|
|
|
ROLE_TYPES = {
|
|
"global": 1,
|
|
"individual": 2,
|
|
"group": 3,
|
|
}
|
|
|
|
PERMISSION_IDS = {
|
|
"users.view": 0x0101,
|
|
"users.create": 0x0103,
|
|
"users.invite": 0x0104,
|
|
"users.delete": 0x0105,
|
|
"users.enable_disable": 0x0106,
|
|
"users.edit_email": 0x0107,
|
|
"users.edit_password": 0x0108,
|
|
"users.edit_note": 0x0109,
|
|
"users.manage_2fa": 0x010A,
|
|
"users.force_logout": 0x010B,
|
|
"users.change_group": 0x010C,
|
|
"users.change_strategy": 0x010D,
|
|
"users.change_control_role": 0x010E,
|
|
"users.edit_display_name": 0x010F,
|
|
"devices.view": 0x0201,
|
|
"devices.enable_disable": 0x0203,
|
|
"devices.delete": 0x0204,
|
|
"devices.edit_info": 0x0205,
|
|
"devices.assign_to_user": 0x0206,
|
|
"devices.change_group": 0x0207,
|
|
"devices.change_strategy": 0x0208,
|
|
"user_groups.view": 0x0301,
|
|
"user_groups.edit": 0x0302,
|
|
"device_groups.view": 0x0401,
|
|
"device_groups.edit": 0x0402,
|
|
"device_groups.change_strategy": 0x0403,
|
|
"audits.view": 0x0501,
|
|
"audits.edit": 0x0502,
|
|
"strategies.view": 0x0601,
|
|
"strategies.edit": 0x0602,
|
|
"custom_clients.view": 0x0701,
|
|
"custom_clients.edit": 0x0702,
|
|
"control_roles.view": 0x0801,
|
|
"control_roles.edit": 0x0802,
|
|
}
|
|
|
|
PERMISSION_NAMES = {permission_id: name for name, permission_id in PERMISSION_IDS.items()}
|
|
|
|
|
|
def check_response(response):
|
|
if response.status_code != 200:
|
|
print(f"Error: HTTP {response.status_code}: {response.text}")
|
|
exit(1)
|
|
|
|
if response.text and response.text.strip():
|
|
try:
|
|
data = response.json()
|
|
except ValueError:
|
|
return response.text
|
|
if isinstance(data, dict) and "error" in data:
|
|
print(f"Error: {data['error']}")
|
|
exit(1)
|
|
return data
|
|
return None
|
|
|
|
|
|
def headers_with(token):
|
|
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
|
|
|
|
def split_csv(value):
|
|
if value is None:
|
|
return None
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
|
|
|
|
def parse_permissions(value):
|
|
permissions = []
|
|
for item in split_csv(value) or []:
|
|
permission = PERMISSION_IDS.get(item.lower())
|
|
if permission is None:
|
|
try:
|
|
permission = int(item, 0)
|
|
except ValueError:
|
|
print(f"Error: Invalid permission name or ID '{item}'")
|
|
exit(1)
|
|
if permission < 0 or permission > 65535:
|
|
print(f"Error: Permission ID '{item}' is outside the 0-65535 range")
|
|
exit(1)
|
|
permissions.append(permission)
|
|
return permissions
|
|
|
|
|
|
def format_role_permissions(role):
|
|
permissions = role.get("permissions")
|
|
if isinstance(permissions, list):
|
|
role["permissions"] = [
|
|
PERMISSION_NAMES.get(permission, permission) for permission in permissions
|
|
]
|
|
return role
|
|
|
|
|
|
def list_roles(url, token, name=None, role_type=None, page_size=50):
|
|
params = {"pageSize": page_size}
|
|
if name is not None:
|
|
params["name"] = name
|
|
if role_type is not None:
|
|
params["type"] = ROLE_TYPES[role_type]
|
|
|
|
roles = []
|
|
current = 0
|
|
while True:
|
|
current += 1
|
|
params["current"] = current
|
|
response = requests.get(
|
|
f"{url}/api/admin-roles", headers=headers_with(token), params=params
|
|
)
|
|
data = check_response(response)
|
|
if not isinstance(data, dict):
|
|
print("Error: Unexpected response while listing admin roles")
|
|
exit(1)
|
|
rows = data.get("data", [])
|
|
roles.extend(format_role_permissions(role) for role in rows)
|
|
total = data.get("total", 0)
|
|
if len(rows) < page_size or current * page_size >= total:
|
|
break
|
|
return roles
|
|
|
|
|
|
def get_role(url, token, name=None, guid=None):
|
|
if guid:
|
|
response = requests.get(
|
|
f"{url}/api/admin-roles/{guid}", headers=headers_with(token)
|
|
)
|
|
role = check_response(response)
|
|
if isinstance(role, dict):
|
|
return format_role_permissions(role)
|
|
return role
|
|
|
|
roles = list_roles(url, token, name=name)
|
|
for role in roles:
|
|
if role.get("name") == name:
|
|
return role
|
|
return None
|
|
|
|
|
|
def resolve_role(url, token, name=None, guid=None):
|
|
role = get_role(url, token, name=name, guid=guid)
|
|
if role:
|
|
return role
|
|
target = guid if guid else name
|
|
print(f"Error: Admin role '{target}' not found")
|
|
exit(1)
|
|
|
|
|
|
def get_user_guid(url, token, name):
|
|
response = requests.get(
|
|
f"{url}/api/users",
|
|
headers=headers_with(token),
|
|
params={"name": name, "pageSize": 50, "current": 1},
|
|
)
|
|
data = check_response(response)
|
|
users = data.get("data", []) if isinstance(data, dict) else []
|
|
for user in users:
|
|
if user.get("name") == name:
|
|
return user.get("guid")
|
|
return None
|
|
|
|
|
|
def resolve_users(url, token, users):
|
|
guids = []
|
|
for user in users:
|
|
if len(user) == 36 and user.count("-") == 4:
|
|
guids.append(user)
|
|
continue
|
|
guid = get_user_guid(url, token, user)
|
|
if not guid:
|
|
print(f"Error: User '{user}' not found")
|
|
exit(1)
|
|
guids.append(guid)
|
|
return guids
|
|
|
|
|
|
def create_role(
|
|
url,
|
|
token,
|
|
name,
|
|
role_type,
|
|
permissions,
|
|
note=None,
|
|
user_groups=None,
|
|
device_groups=None,
|
|
unassigned=None,
|
|
):
|
|
payload = {
|
|
"name": name,
|
|
"type": ROLE_TYPES[role_type],
|
|
"permissions": permissions,
|
|
}
|
|
if note is not None:
|
|
payload["note"] = note
|
|
if user_groups:
|
|
payload["user_groups"] = user_groups
|
|
if device_groups:
|
|
payload["device_groups"] = device_groups
|
|
if unassigned is not None:
|
|
payload["unassigned"] = unassigned
|
|
response = requests.post(
|
|
f"{url}/api/admin-roles", headers=headers_with(token), json=payload
|
|
)
|
|
check_response(response)
|
|
|
|
|
|
def update_role(
|
|
url,
|
|
token,
|
|
guid,
|
|
new_name=None,
|
|
note=None,
|
|
permissions=None,
|
|
user_groups=None,
|
|
device_groups=None,
|
|
unassigned=None,
|
|
):
|
|
payload = {}
|
|
if new_name is not None:
|
|
payload["name"] = new_name
|
|
if note is not None:
|
|
payload["note"] = note
|
|
if permissions is not None:
|
|
payload["permissions"] = permissions
|
|
if user_groups is not None:
|
|
payload["user_groups"] = user_groups
|
|
if device_groups is not None:
|
|
payload["device_groups"] = device_groups
|
|
if unassigned is not None:
|
|
payload["unassigned"] = unassigned
|
|
response = requests.put(
|
|
f"{url}/api/admin-roles/{guid}", headers=headers_with(token), json=payload
|
|
)
|
|
check_response(response)
|
|
|
|
|
|
def delete_roles(url, token, guids):
|
|
response = requests.delete(
|
|
f"{url}/api/admin-roles",
|
|
headers=headers_with(token),
|
|
json={"guids": guids},
|
|
)
|
|
check_response(response)
|
|
|
|
|
|
def change_users(url, token, guid, users, remove=False):
|
|
method = requests.delete if remove else requests.post
|
|
response = method(
|
|
f"{url}/api/admin-roles/{guid}/users",
|
|
headers=headers_with(token),
|
|
json={"users": users},
|
|
)
|
|
check_response(response)
|
|
|
|
|
|
def view_users(url, token, role_guid, page_size=50):
|
|
params = {"admin_role_guid": role_guid, "pageSize": page_size}
|
|
users = []
|
|
current = 0
|
|
while True:
|
|
current += 1
|
|
params["current"] = current
|
|
response = requests.get(
|
|
f"{url}/api/users", headers=headers_with(token), params=params
|
|
)
|
|
data = check_response(response)
|
|
if not isinstance(data, dict):
|
|
print("Error: Unexpected response while listing users")
|
|
exit(1)
|
|
rows = data.get("data", [])
|
|
users.extend(rows)
|
|
total = data.get("total", 0)
|
|
if len(rows) < page_size or current * page_size >= total:
|
|
break
|
|
return users
|
|
|
|
|
|
def require_role_target(parser, args):
|
|
if not args.name and not args.guid:
|
|
parser.error("one of --name or --guid is required")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Admin role manager")
|
|
parser.add_argument(
|
|
"command",
|
|
choices=["view", "add", "update", "delete", "view-users", "add-users", "remove-users"],
|
|
)
|
|
parser.add_argument("--url", required=True, help="Server URL")
|
|
parser.add_argument("--token", required=True, help="API token")
|
|
parser.add_argument("--name", help="Admin role name")
|
|
parser.add_argument("--guid", help="Admin role GUID")
|
|
parser.add_argument("--new-name", help="New admin role name")
|
|
parser.add_argument("--note", help="Role note; use an empty value to clear it")
|
|
parser.add_argument("--type", choices=ROLE_TYPES, help="Role type")
|
|
parser.add_argument(
|
|
"--permissions",
|
|
help="Comma-separated permission names or numeric IDs; use an empty value to clear",
|
|
)
|
|
parser.add_argument(
|
|
"--user-groups",
|
|
help="Comma-separated user group names; use an empty value to clear",
|
|
)
|
|
parser.add_argument(
|
|
"--device-groups",
|
|
help="Comma-separated device group names; use an empty value to clear",
|
|
)
|
|
parser.add_argument("--users", help="Comma-separated user names or GUIDs")
|
|
unassigned = parser.add_mutually_exclusive_group()
|
|
unassigned.add_argument(
|
|
"--unassigned", dest="unassigned", action="store_true", help="Include unassigned devices"
|
|
)
|
|
unassigned.add_argument(
|
|
"--no-unassigned",
|
|
dest="unassigned",
|
|
action="store_false",
|
|
help="Exclude unassigned devices",
|
|
)
|
|
parser.set_defaults(unassigned=None)
|
|
args = parser.parse_args()
|
|
args.url = args.url.rstrip("/")
|
|
|
|
if args.command == "view":
|
|
if args.guid:
|
|
result = resolve_role(args.url, args.token, guid=args.guid)
|
|
else:
|
|
result = list_roles(args.url, args.token, args.name, args.type)
|
|
print(json.dumps(result, indent=2))
|
|
return
|
|
|
|
if args.command == "add":
|
|
if not args.name or not args.type or args.permissions is None:
|
|
parser.error("--name, --type, and --permissions are required for add")
|
|
if args.type != "group" and (
|
|
args.user_groups is not None
|
|
or args.device_groups is not None
|
|
or args.unassigned is not None
|
|
):
|
|
parser.error("group scope options can only be used with --type group")
|
|
create_role(
|
|
args.url,
|
|
args.token,
|
|
args.name,
|
|
args.type,
|
|
parse_permissions(args.permissions),
|
|
args.note,
|
|
split_csv(args.user_groups),
|
|
split_csv(args.device_groups),
|
|
args.unassigned,
|
|
)
|
|
print(f"Success: Created admin role '{args.name}'")
|
|
return
|
|
|
|
require_role_target(parser, args)
|
|
role = resolve_role(args.url, args.token, args.name, args.guid)
|
|
role_guid = role.get("guid")
|
|
role_name = role.get("name")
|
|
|
|
if args.command == "update":
|
|
updates = [
|
|
args.new_name,
|
|
args.note,
|
|
args.permissions,
|
|
args.user_groups,
|
|
args.device_groups,
|
|
args.unassigned,
|
|
]
|
|
if all(value is None for value in updates):
|
|
parser.error("at least one update option is required")
|
|
if role.get("type") != ROLE_TYPES["group"] and (
|
|
args.user_groups is not None
|
|
or args.device_groups is not None
|
|
or args.unassigned is not None
|
|
):
|
|
parser.error("group scope options can only be used with a group role")
|
|
update_role(
|
|
args.url,
|
|
args.token,
|
|
role_guid,
|
|
args.new_name,
|
|
args.note,
|
|
parse_permissions(args.permissions) if args.permissions is not None else None,
|
|
split_csv(args.user_groups),
|
|
split_csv(args.device_groups),
|
|
args.unassigned,
|
|
)
|
|
print(f"Success: Updated admin role '{role_name}'")
|
|
elif args.command == "delete":
|
|
delete_roles(args.url, args.token, [role_guid])
|
|
print(f"Success: Deleted admin role '{role_name}'")
|
|
elif args.command == "view-users":
|
|
print(json.dumps(view_users(args.url, args.token, role_guid), indent=2))
|
|
elif args.command in ("add-users", "remove-users"):
|
|
users = split_csv(args.users)
|
|
if not users:
|
|
parser.error("--users is required for add-users and remove-users")
|
|
user_guids = resolve_users(args.url, args.token, users)
|
|
remove = args.command == "remove-users"
|
|
change_users(args.url, args.token, role_guid, user_guids, remove=remove)
|
|
action = "Removed users from" if remove else "Added users to"
|
|
print(f"Success: {action} admin role '{role_name}'")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|