diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index 2bd8c61..b8bf362 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -5179,6 +5179,16 @@ private struct WorkbenchAccountManagerView: View { @State private var password = "" @State private var databaseName = "" @State private var privilege = "SELECT" + @State private var selectedPrivileges: Set = ["SELECT"] + @State private var privilegeScope = "Schema" + @State private var selectedRole = "Custom" + private let rolePresets: [(String, String, [String])] = [ + ("Custom", "Choose individual privileges", []), + ("Read Only", "Read tables and views", ["SELECT", "SHOW VIEW"]), + ("Read / Write", "Read and modify records", ["SELECT", "INSERT", "UPDATE", "DELETE", "SHOW VIEW"]), + ("DB Designer", "Create and alter schema objects", ["SELECT", "CREATE", "ALTER", "DROP", "INDEX", "REFERENCES", "CREATE VIEW", "SHOW VIEW", "TRIGGER"]), + ("DB Manager", "Manage all supported schema privileges", ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "INDEX", "REFERENCES", "EXECUTE", "CREATE VIEW", "SHOW VIEW", "TRIGGER"]) + ] @State private var previewSQL = "" @State private var confirm = false @State private var running = false @@ -5217,12 +5227,48 @@ private struct WorkbenchAccountManagerView: View { SecureField("Password", text: $password) } if action == .grant || action == .revoke { - TextField("Database", text: $databaseName) - Picker("Privilege", selection: $privilege) { - ForEach(privileges, id: \.self) { item in - Text(item).tag(item) + Picker("Privilege scope", selection: $privilegeScope) { + Text("Schema").tag("Schema") + Text("Global").tag("Global") + } + if privilegeScope == "Schema" { + TextField("Schema name", text: $databaseName) + .help("Enter an existing database name. Privileges apply to all objects in this schema.") + } else { + Text("Global privileges affect every database on this server. Review carefully before applying.") + .foregroundStyle(.orange) + } + Picker("Role preset", selection: $selectedRole) { + ForEach(rolePresets.map { $0.0 }, id: \.self) { role in + Text(role).tag(role) } } + .onChange(of: selectedRole) { _, newRole in + if let preset = rolePresets.first(where: { $0.0 == newRole }), newRole != "Custom" { + selectedPrivileges = Set(preset.2) + } + } + Text("Select the privileges to \(action == .grant ? "grant" : "revoke")") + .font(.headline) + LazyVGrid(columns: [GridItem(.adaptive(minimum: 165), spacing: 10)], alignment: .leading, spacing: 12) { + ForEach(privileges, id: \.self) { item in + Toggle(item, isOn: Binding( + get: { selectedPrivileges.contains(item) }, + set: { enabled in + if enabled { selectedPrivileges.insert(item) } + else { selectedPrivileges.remove(item) } + selectedRole = "Custom" + } + )) + .toggleStyle(.checkbox) + } + } + HStack { + Button("Select All") { selectedPrivileges = Set(privileges); selectedRole = "Custom" } + Button("Clear") { selectedPrivileges.removeAll(); selectedRole = "Custom" } + Spacer() + Text("\(selectedPrivileges.count) selected").foregroundStyle(.secondary) + } } } .formStyle(.grouped) @@ -5241,7 +5287,7 @@ private struct WorkbenchAccountManagerView: View { } } .padding(20) - .frame(width: 590) + .frame(minWidth: 650, idealWidth: 760, minHeight: 520) .onAppear { if let selectedAccount { let parts = selectedAccount.split(separator: "\u{001f}", omittingEmptySubsequences: false) @@ -5285,15 +5331,23 @@ private struct WorkbenchAccountManagerView: View { case .unlock: return ("ALTER USER \(account) ACCOUNT UNLOCK", "ALTER USER \(account) ACCOUNT UNLOCK") case .grant, .revoke: - guard !databaseName.isEmpty, - databaseName.unicodeScalars.allSatisfy({ allowed.contains($0) }), - privileges.contains(privilege) else { - throw AccountError.invalid("Enter a valid database name and privilege.") + guard !selectedPrivileges.isEmpty, selectedPrivileges.allSatisfy({ privileges.contains($0) }) else { + throw AccountError.invalid("Select at least one privilege.") } - let db = "`\(databaseName)`.*" + let target: String + if privilegeScope == "Global" { + target = "*.*" + } else { + guard !databaseName.isEmpty, + databaseName.unicodeScalars.allSatisfy({ allowed.contains($0) }) else { + throw AccountError.invalid("Enter a valid schema name.") + } + target = "`\(databaseName.replacingOccurrences(of: "`", with: "``"))`.*" + } + let rights = selectedPrivileges.sorted().joined(separator: ", ") let command = action == .grant - ? "GRANT \(privilege) ON \(db) TO \(account)" - : "REVOKE \(privilege) ON \(db) FROM \(account)" + ? "GRANT \(rights) ON \(target) TO \(account)" + : "REVOKE \(rights) ON \(target) FROM \(account)" return (command, command) } }