diff --git a/dirsrvtests/tests/suites/config/compact_test.py b/dirsrvtests/tests/suites/config/compact_test.py index 3cc50963e8..b0e57dd9c5 100644 --- a/dirsrvtests/tests/suites/config/compact_test.py +++ b/dirsrvtests/tests/suites/config/compact_test.py @@ -86,6 +86,13 @@ def test_compaction_interval_and_time(topo): now = datetime.datetime.now() current_hour = now.hour current_minute = now.minute + 2 + + if current_minute >= 60: + # handle time wrapping/rollover + current_minute = current_minute - 60 + # Bump to the next hour + current_hour += 1 + if current_hour < 10: hour = "0" + str(current_hour) else: @@ -94,6 +101,7 @@ def test_compaction_interval_and_time(topo): minute = "0" + str(current_minute) else: minute = str(current_minute) + compact_time = hour + ":" + minute # Set compaction TOD @@ -102,10 +110,10 @@ def test_compaction_interval_and_time(topo): inst.deleteErrorLogs(restart=True) # Check compaction occurred as expected - time.sleep(60) + time.sleep(45) assert not inst.searchErrorsLog("Compacting databases") - time.sleep(61) + time.sleep(90) assert inst.searchErrorsLog("Compacting databases") inst.deleteErrorLogs(restart=False) diff --git a/src/cockpit/389-console/src/ds.jsx b/src/cockpit/389-console/src/ds.jsx index 465a4d5d53..ca1c4d0eba 100644 --- a/src/cockpit/389-console/src/ds.jsx +++ b/src/cockpit/389-console/src/ds.jsx @@ -94,6 +94,7 @@ export class DSInstance extends React.Component { backupRows: [], notifications: [], activeTabKey: 1, + createKey: 0, wasActiveList: [], progressValue: 0, loadingOperate: false, @@ -576,8 +577,10 @@ export class DSInstance extends React.Component { } openCreateInstanceModal() { + const key = this.state.createKey + 1; this.setState({ - showCreateInstanceModal: true + showCreateInstanceModal: true, + createKey: key }); } @@ -855,6 +858,7 @@ export class DSInstance extends React.Component { {serverDropdown} {mainPage} { - // Success!!! Now cleanup everything up... - log_cmd("handleCreateInstance", "Instance creation compelete, clean everything up...", rm_cmd); - cockpit.spawn(rm_cmd, { superuser: true }); // Remove Inf file with clear text password - this.setState({ - loadingCreate: false - }); - - loadInstanceList(createServerId); - addNotification( - "success", - `Successfully created instance: slapd-${createServerId}` - ); - closeHandler(); - this.resetModal(); + // Success!!! Now set Root DN pw, and cleanup everything up... + log_cmd("handleCreateInstance", "Instance creation compelete, remove INF file...", rm_cmd); + cockpit.spawn(rm_cmd, { superuser: true }); + + const dm_pw_cmd = ['dsconf', '-j', 'ldapi://%2fvar%2frun%2fslapd-' + newServerId + '.socket', + 'directory_manager', 'password_change']; + const config = { + cmd: dm_pw_cmd, + promptArg: "", + passwd: createDMPassword, + addNotification: addNotification, + success_msg: `Successfully created instance: slapd-${createServerId}`, + error_msg: "Failed to set Directory Manager password", + state_callback: () => { this.setState({ loadingCreate: false }) }, + reload_func: loadInstanceList, + reload_arg: createServerId, + ext_func: closeHandler, + ext_arg: "", + funcName: "handleCreateInstance", + funcDesc: "Set Directory Manager password..." + }; + callCmdStreamPassword(config); }); }); }); diff --git a/src/cockpit/389-console/src/lib/database/chaining.jsx b/src/cockpit/389-console/src/lib/database/chaining.jsx index e80ee3544e..9ac5682df9 100644 --- a/src/cockpit/389-console/src/lib/database/chaining.jsx +++ b/src/cockpit/389-console/src/lib/database/chaining.jsx @@ -1,7 +1,7 @@ import cockpit from "cockpit"; import React from "react"; import { DoubleConfirmModal } from "../notifications.jsx"; -import { log_cmd } from "../tools.jsx"; +import { log_cmd, callCmdStreamPassword } from "../tools.jsx"; import { Button, Checkbox, @@ -1162,6 +1162,7 @@ export class ChainingConfig extends React.Component { saveLink() { const missingArgs = {}; + let bind_pw = ""; let errors = false; if (this.state.nsfarmserverurl == "") { @@ -1225,7 +1226,7 @@ export class ChainingConfig extends React.Component { cmd.push('--bind-dn=' + this.state.nsmultiplexorbinddn); } if (this.state.nsmultiplexorcredentials != this.state._nsmultiplexorcredentials) { - cmd.push('--bind-pw=' + this.state.nsmultiplexorcredentials); + bind_pw = this.state.nsmultiplexorcredentials } if (this.state.timelimit != this.state._timelimit) { cmd.push('--time-limit=' + this.state.timelimit); @@ -1298,30 +1299,20 @@ export class ChainingConfig extends React.Component { saving: true }); // Something changed, perform the update - log_cmd("saveLink", "Save chaining link config", cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.reload(this.props.suffix); - this.props.addNotification( - "success", - `Successfully Updated Link Configuration` - ); - this.setState({ - saving: false - }); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.reload(this.props.suffix); - this.props.addNotification( - "error", - `Failed to update link configuration - ${errMsg.desc}` - ); - this.setState({ - saving: false - }); - }); + const config = { + cmd: cmd, + promptArg: "--bind-pw-prompt", + passwd: bind_pw, + addNotification: this.props.addNotification, + success_msg: "Successfully Updated Link Configuration", + error_msg: "Failed to update link configuration", + state_callback: () => { this.setState({ saving: false }) }, + reload_func: this.props.reload, + reload_arg: this.props.suffix, + funcName: "saveLink", + funcDesc: "Save chaining link config" + }; + callCmdStreamPassword(config); } } diff --git a/src/cockpit/389-console/src/lib/database/databaseConfig.jsx b/src/cockpit/389-console/src/lib/database/databaseConfig.jsx index 4f770eb2ca..92f260c32e 100644 --- a/src/cockpit/389-console/src/lib/database/databaseConfig.jsx +++ b/src/cockpit/389-console/src/lib/database/databaseConfig.jsx @@ -398,7 +398,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} unit="%" /> @@ -422,7 +422,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -623,7 +623,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -646,7 +646,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -669,7 +669,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -692,7 +692,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -715,7 +715,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -810,7 +810,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -922,7 +922,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> @@ -945,7 +945,7 @@ export class GlobalDatabaseConfig extends React.Component { inputAriaLabel="number input" minusBtnAriaLabel="minus" plusBtnAriaLabel="plus" - widthChars={8} + widthChars={10} /> diff --git a/src/cockpit/389-console/src/lib/ldap_editor/lib/editableTable.jsx b/src/cockpit/389-console/src/lib/ldap_editor/lib/editableTable.jsx index 9463de61a6..d7ccd0eaa3 100644 --- a/src/cockpit/389-console/src/lib/ldap_editor/lib/editableTable.jsx +++ b/src/cockpit/389-console/src/lib/ldap_editor/lib/editableTable.jsx @@ -60,7 +60,7 @@ class EditableTable extends React.Component { showPassword: false, pwdValue: "", pwdRowIndex: -1, - fileValue: null, + fileValue: "", fileName: '', encodedValueIsEmpty: true, strPathValueIsEmpty: true, @@ -159,28 +159,31 @@ class EditableTable extends React.Component { } }; - this.handleFileChange = (fileValue, fileName, event) => { + this.handleClear = () => { + this.setState({ + fileName: "" + }); + }; + + this.handleFileChange = (e, file) => { let encodedValue; const encodedValueIsEmpty = true; const strPathValueIsEmpty = true; const isFileTooLarge = false; this.setState({ - fileValue, - fileName, + fileName: file.name, encodedValueIsEmpty, strPathValueIsEmpty, isFileTooLarge }); - console.log(`fileValue = #${fileValue}#`); - console.log(fileValue); - console.log(`fileValue.size = ${fileValue.size}`); + console.debug('handleFileChange - file: ', file); - if (fileValue.size === undefined) { // The "Clear" button was pressed. + if (file.size === undefined) { // The "Clear" button was pressed. return; } - if (fileValue.size === 0) { // An empty file was selected. + if (file.size === 0) { // An empty file was selected. console.log('An empty file was selected. Nothing to do.'); return; } @@ -191,8 +194,8 @@ class EditableTable extends React.Component { // // https://github.com/cockpit-project/cockpit/blob/dee6324d037f3b8961d1b38960b4226c7e473abf/src/websocket/websocketconnection.c#L154 // - if (fileValue.size > WEB_SOCKET_MAX_PAYLOAD) { - console.log('File too large!'); + if (file.size > WEB_SOCKET_MAX_PAYLOAD) { + console.log('handleFileChange - File too large!'); this.setState({ isFileTooLarge: true }); return; } @@ -200,32 +203,34 @@ class EditableTable extends React.Component { // data:image/png;base64, // base64encode(fileName, val => { console.log(val); }) // https://stackoverflow.com/questions/36280818/how-to-convert-file-to-base64-in-javascript + const reader = new FileReader(); - reader.readAsDataURL(fileValue); + reader.readAsDataURL(file); reader.onload = () => { const pattern = ';base64,'; const pos = reader.result.indexOf(pattern); const toDel = reader.result.substring(0, pos + pattern.length); - const isAnImage = fileValue.type.startsWith('image/'); + const isAnImage = file.type.startsWith('image/'); // Check the file type. encodedValue = this.state.attrIsJpegPhoto ? isAnImage ? reader.result.replace(toDel, '') - : null + : "" // The attribute is a certificate. - : (fileValue.type === 'application/x-x509-ca-cert') || - (fileValue.type === 'application/x-x509-user-cert') + : (file.type === 'application/x-x509-ca-cert') || + (file.type === 'application/x-x509-user-cert') || + (file.type === 'application/pkix-cert') ? reader.result.replace(toDel, '') - : null; + : ""; - if (encodedValue === null) { - console.log('encodedValue is null. Nothing to do.'); + if (encodedValue === "") { + console.log('handleFileChange - encodedValue is null. Nothing to do.'); return; } // Decode the binary value. - let myDecodedValue = null; + let myDecodedValue = ""; if (isAnImage) { if (this.state.attrIsJpegPhoto) { myDecodedValue = ( Decode the certificate // IMPORTANT! ==> Enable the "Confirm" button once the cert decoding is completed. // myDecodedValue = ... + myDecodedValue = (
+ +
); } // console.log(reader.result.substring(0, 100)); - console.log(`encodedValue.substring(0, 100) = ${encodedValue.substring(0, 100)}`); + console.log(`handleFileChange - encodedValue.substring(0, 100) = ${encodedValue.substring(0, 100)}`); const newRows = [...this.state.tableRows]; newRows[this.state.currentRowIndex].cells[1].props.value = myDecodedValue; // Store the encoded value to use it to create the LDIF statements! @@ -264,7 +274,7 @@ class EditableTable extends React.Component { this.props.saveCurrentRows(rowDataToSave, this.state.namingRowID); }; reader.onerror = (error) => { - console.log(`Failed to encode the file : ${fileName}`, error); + console.log(`handleFileChange - Failed to encode the file : ${file.name}`, error); }; }; @@ -819,22 +829,20 @@ class EditableTable extends React.Component { The certificate must be stored in the Distinguished Encoding Rules (DER) format. } - { uploadSelected && } - { !uploadSelected && - currentLine.substring(0, 1000) + const myTruncatedValue = (
diff --git a/src/cockpit/389-console/src/lib/ldap_editor/treeView.jsx b/src/cockpit/389-console/src/lib/ldap_editor/treeView.jsx index 499ef8331c..49d92fee43 100644 --- a/src/cockpit/389-console/src/lib/ldap_editor/treeView.jsx +++ b/src/cockpit/389-console/src/lib/ldap_editor/treeView.jsx @@ -210,53 +210,58 @@ class EditorTreeView extends React.Component { .filter(data => (data.attribute + data.value !== '') && // Filter out empty lines (data.attribute !== '???: ')) // and data for empty suffix(es) and in case of failure. .map(line => { - const attr = line.attribute; - const attrLowerCase = attr.trim().toLowerCase(); - let val = line.value.substring(1); - - if (line.value.substring(0, 2) === '::') { - if (this.attributesSpecialHandling.includes(attrLowerCase)) { - // Let the encoded value be added to the table first. - // Keep the index where the value will be inserted ( current length of the array). - // Once the decoding is done, insert the decoded value at the relevant index. - encodedValues.push({ index: entryRows.length, line: line }); - } else { - // TODO: Check why the decoding of 'nssymmetrickey is failing... - if (attrLowerCase === 'nssymmetrickey') { - val = line.value.substring(3); + if (line.attribute !== undefined) { + const attr = line.attribute; + const attrLowerCase = attr.trim().toLowerCase(); + let val = line.value.substring(1); + + if (line.value.substring(0, 2) === '::') { + if (this.attributesSpecialHandling.includes(attrLowerCase)) { + // Let the encoded value be added to the table first. + // Keep the index where the value will be inserted ( current length of the array). + // Once the decoding is done, insert the decoded value at the relevant index. + encodedValues.push({ index: entryRows.length, line: line }); } else { - val = b64DecodeUnicode(line.value.substring(3)); + // TODO: Check why the decoding of 'nssymmetrickey is failing... + if (attrLowerCase === 'nssymmetrickey') { + val = line.value.substring(3); + } else { + val = b64DecodeUnicode(line.value.substring(3)); + } } } - } - if (attr.toLowerCase() === "userpassword") { - val = "********"; - } + if (attr.toLowerCase() === "userpassword") { + val = "********"; + } - entryRows.push([{ title: {attr} }, val]); - const myVal = val.trim().toLowerCase(); - const accountObjectclasses = ['nsaccount', 'nsperson', 'simplesecurityobject', - 'organization', 'person', 'account', 'organizationalunit', - 'netscapeserver', 'domain', 'posixaccount', 'shadowaccount', - 'posixgroup', 'mailrecipient', 'nsroledefinition']; - if (accountObjectclasses.includes(myVal)) { - entryStateIcon = - } - if (myVal === 'nsroledefinition') { - isRole = true; - } - // TODO: Use a better logic to assign icons! - // console.log(`!entryIcon = ${!entryIcon}`); - if (!entryIcon && attrLowerCase === 'objectclass') { - // console.log(`val.trim().toLowerCase() = ${val.trim().toLowerCase()}`); - if (myVal === 'inetorgperson' || myVal === 'posixaccount' || myVal === 'person') { - entryIcon = - } else if (myVal === 'organizationalunit' || myVal === 'groupofuniquenames' || myVal === 'groupofnames') { - entryIcon = - } else if (myVal === 'domain') { - entryIcon = + entryRows.push([{ title: {attr} }, val]); + const myVal = val.trim().toLowerCase(); + const accountObjectclasses = ['nsaccount', 'nsperson', 'simplesecurityobject', + 'organization', 'person', 'account', 'organizationalunit', + 'netscapeserver', 'domain', 'posixaccount', 'shadowaccount', + 'posixgroup', 'mailrecipient', 'nsroledefinition']; + if (accountObjectclasses.includes(myVal)) { + entryStateIcon = + } + if (myVal === 'nsroledefinition') { + isRole = true; + } + // TODO: Use a better logic to assign icons! + // console.log(`!entryIcon = ${!entryIcon}`); + if (!entryIcon && attrLowerCase === 'objectclass') { + // console.log(`val.trim().toLowerCase() = ${val.trim().toLowerCase()}`); + if (myVal === 'inetorgperson' || myVal === 'posixaccount' || myVal === 'person') { + entryIcon = + } else if (myVal === 'organizationalunit' || myVal === 'groupofuniquenames' || myVal === 'groupofnames') { + entryIcon = + } else if (myVal === 'domain') { + entryIcon = + } } + } else { + // Value too large Label + entryRows.push([{ title: {line.props.attr} }, line]); } }); @@ -361,11 +366,11 @@ class EditorTreeView extends React.Component { this.setState({ entryIcon: myPhoto }); break; } - - case 'usercertificate': - case 'usercertificate;binary': case 'userpassword': numberDecoded++; + break; + case 'usercertificate': + case 'usercertificate;binary': case 'cacertificate': case 'cacertificate;binary': showCertificate(encVal, diff --git a/src/cockpit/389-console/src/lib/ldap_editor/wizards/operations/addUser.jsx b/src/cockpit/389-console/src/lib/ldap_editor/wizards/operations/addUser.jsx index 1c955dfcf8..6a90a6a4c2 100644 --- a/src/cockpit/389-console/src/lib/ldap_editor/wizards/operations/addUser.jsx +++ b/src/cockpit/389-console/src/lib/ldap_editor/wizards/operations/addUser.jsx @@ -8,6 +8,7 @@ import { Grid, GridItem, Label, Pagination, + SearchInput, Select, SelectOption, SelectVariant, SimpleList, SimpleListItem, Spinner, @@ -136,6 +137,7 @@ class AddUser extends React.Component { savedRows: [], commandOutput: '', resultVariant: 'default', + searchValue: "", stepIdReached: 1, itemCountAddUser: 0, pageAddUser: 1, @@ -144,6 +146,7 @@ class AddUser extends React.Component { { title: 'Attribute Name', cellTransforms: [headerCol()] }, ], rowsUser: [], + rowsOrig: [], pagedRowsUser: [], selectedAttributes: [], isAttrDropDownOpen: false, @@ -239,6 +242,7 @@ class AddUser extends React.Component { this.setState({ itemCountAddUser: attributesArray.length, rowsUser: attributesArray, + rowsOrig: [...attributesArray], pagedRowsUser: attributesArray.slice(0, this.state.perPageAddUser), accountType: selection, selectedAttributes: selectedAttrs, @@ -246,6 +250,34 @@ class AddUser extends React.Component { }); } + this.onAttrSearchChange = (value, event) => { + let attrRows = []; + let allAttrs = []; + const val = value.toLowerCase(); + + allAttrs = this.state.rowsOrig; + + // Process search filter on the entire list + if (val !== "") { + for (const row of allAttrs) { + const name = row.cells[0].toLowerCase(); + if (name.includes(val)) { + attrRows.push(row); + } + } + } else { + // Restore entire row list + attrRows = allAttrs; + } + + this.setState({ + rowsUser: attrRows, + pagedRowsUser: attrRows.slice(0, this.state.perPageAttr), + itemCountAddUser: attrRows.length, + }) + } + + // End constructor(). } @@ -270,6 +302,7 @@ class AddUser extends React.Component { this.setState({ itemCountAddUser: attributesArray.length, rowsUser: attributesArray, + rowsOrig: [...attributesArray], pagedRowsUser: attributesArray.slice(0, this.state.perPageAddUser), selectedAttributes: [...this.requiredAttrs[this.state.accountType]], }); @@ -321,9 +354,10 @@ class AddUser extends React.Component { // The row ID cannot be used since it changes with the pagination. const attrName = this.state.pagedRowsUser[rowId].cells[0]; let allItems = [...this.state.rowsUser]; + const allAttrs = this.state.rowsOrig; const index = allItems.findIndex(item => item.cells[0] === attrName); allItems[index].isAttributeSelected = isSelected; - const selectedAttributes = allItems + let selectedAttributes = allAttrs .filter(item => item.isAttributeSelected) .map(selectedAttr => selectedAttr.cells[0]); @@ -593,15 +627,28 @@ class AddUser extends React.Component { {this.buildAttrDropdown()}
- + + + this.onAttrSearchChange('', evt)} + /> + + + + + (data.attribute + data.value !== '' && // Filter out empty lines data.attribute !== '???: ')) // and data for empty suffix(es) and in case of failure. .map((line, index) => { - const obj = {}; - const attr = line.attribute; - const attrLowerCase = attr.trim().toLowerCase(); + let attrLowerCase; let namingAttribute = false; - let val = line.value.substring(1).trim(); - let encodedvalue = ""; - - if (attrLowerCase === "objectclass") { - objectclasses.push(val); - if (val.toLowerCase() === "groupofnames") { - isGroupOfNames = true; - } else if (val.toLowerCase() === "groupofuniquenames") { - isGroupOfUniqueNames = true; - } - } else { - // Base64 encoded values - if (line.attribute === "dn") { - //return; - } - if (line.value.substring(0, 2) === '::') { - val = line.value.substring(3); - if (BINARY_ATTRIBUTES.includes(attrLowerCase)) { - // obj.fileUpload = true; - // obj.isDisabled = true; - if (attrLowerCase === 'jpegphoto') { - const myPhoto = (); - encodedvalue = val; - val = myPhoto; - } else if (attrLowerCase === 'nssymmetrickey') { - // TODO: Check why the decoding of 'nssymmetrickey is failing... - // https://access.redhat.com/documentation/en-us/red_hat_directory_server/10 - // /html/configuration_command_and_file_reference/core_server_configuration_reference#cnchangelog5-nsSymmetricKey - // - // Just show the encoded value at the moment. - val = line.value.substring(3); - } - } else { // The value likely contains accented characters or has a trailing space. - val = b64DecodeUnicode(line.value.substring(3)); + if (line.attribute !== undefined) { + const obj = {}; + const attr = line.attribute; + attrLowerCase = attr.trim().toLowerCase(); + let val = line.value.substring(1).trim(); + let encodedvalue = ""; + + if (attrLowerCase === "objectclass") { + objectclasses.push(val); + if (val.toLowerCase() === "groupofnames") { + isGroupOfNames = true; + } else if (val.toLowerCase() === "groupofuniquenames") { + isGroupOfUniqueNames = true; } } else { - // Check for naming attribute - if (attr === rdnInfo.rdnAttr && val === rdnInfo.rdnVal) { - namingAttribute = true; - namingAttr = attr; - namingValue = val; + // Base64 encoded values + if (line.attribute === "dn") { + //return; + } + if (line.value.substring(0, 2) === '::') { + val = line.value.substring(3); + if (BINARY_ATTRIBUTES.includes(attrLowerCase)) { + // obj.fileUpload = true; + // obj.isDisabled = true; + if (attrLowerCase === 'jpegphoto') { + const myPhoto = (); + encodedvalue = val; + val = myPhoto; + } else if (attrLowerCase === 'nssymmetrickey') { + // TODO: Check why the decoding of 'nssymmetrickey is failing... + // https://access.redhat.com/documentation/en-us/red_hat_directory_server/10 + // /html/configuration_command_and_file_reference/core_server_configuration_reference#cnchangelog5-nsSymmetricKey + // + // Just show the encoded value at the moment. + val = line.value.substring(3); + } + } else { // The value likely contains accented characters or has a trailing space. + val = b64DecodeUnicode(line.value.substring(3)); + } + } else { + // Check for naming attribute + if (attr === rdnInfo.rdnAttr && val === rdnInfo.rdnVal) { + namingAttribute = true; + namingAttr = attr; + namingValue = val; + } } - } + obj.id = generateUniqueId(); + obj.attr = attr; + obj.val = val; + obj.encodedvalue = encodedvalue; + obj.namingAttr = namingAttribute; + obj.required = namingAttribute; + this.originalEntryRows.push(obj); + } + // Handle group members separately + if (attrLowerCase === "member" || attrLowerCase === "uniquemember") { + members.push(val); + } + } else { + // Value too large Label + const obj = {}; obj.id = generateUniqueId(); - obj.attr = attr; - obj.val = val; - obj.encodedvalue = encodedvalue; - obj.namingAttr = namingAttribute; - obj.required = namingAttribute; + obj.attr = line.props.attr; + obj.val = line; + obj.encodedvalue = ""; + obj.namingAttr = false; + obj.required = false; + attrLowerCase = line.props.attr.trim().toLowerCase(); this.originalEntryRows.push(obj); } - - // Handle groupo members separately - if (attrLowerCase === "member" || attrLowerCase === "uniquemember") { - members.push(val); - } }); // Mark the existing objectclass classes as selected diff --git a/src/cockpit/389-console/src/lib/replication/replAgmts.jsx b/src/cockpit/389-console/src/lib/replication/replAgmts.jsx index f678040d8f..ad24a5d053 100644 --- a/src/cockpit/389-console/src/lib/replication/replAgmts.jsx +++ b/src/cockpit/389-console/src/lib/replication/replAgmts.jsx @@ -1118,6 +1118,8 @@ export class ReplAgmts extends React.Component { 'dsconf', '-j', 'ldapi://%2fvar%2frun%2fslapd-' + this.props.serverId + '.socket', 'repl-agmt', 'set', this.state.agmtName, '--suffix=' + this.props.suffix, ]; + let passwd = ""; + let bootstrap_passwd = ""; // Handle Schedule if (this.state.agmtSync) { @@ -1155,7 +1157,7 @@ export class ReplAgmts extends React.Component { cmd.push('--conn-protocol=' + this.state.agmtProtocol); } if (this.state.agmtBindPW != this.state._agmtBindPW) { - cmd.push('--bind-passwd=' + this.state.agmtBindPW); + passwd = this.state.agmtBindPW; } if (this.state.agmtBindDN != this.state._agmtBindDN) { cmd.push('--bind-dn=' + this.state.agmtBindDN); @@ -1183,7 +1185,7 @@ export class ReplAgmts extends React.Component { cmd.push('--bootstrap-conn-protocol=' + this.state.agmtBootstrapProtocol); } if (this.state.agmtBootstrapBindPW != this.state._agmtBootstrapBindPW) { - cmd.push('--bootstrap-bind-passwd=' + this.state.agmtBootstrapBindPW); + bootstrap_passwd = this.state.agmtBootstrapBindPW } if (this.state.agmtBootstrapBindDN != this.state._agmtBootstrapBindDN) { cmd.push('--bootstrap-bind-dn=' + this.state.agmtBootstrapBindDN); @@ -1193,10 +1195,23 @@ export class ReplAgmts extends React.Component { this.setState({ savingAgmt: true, }); - log_cmd('saveAgmt', 'update replication agreement', cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { + + // Update args with password file + if (passwd !== "") { + // Add password file arg + cmd.push("--bind-passwd-prompt"); + } + if (bootstrap_passwd !== "") { + // Add bootstrap password file arg + cmd.push("--bootstrap-bind-passwd-prompt"); + } + + log_cmd('saveAgmt', 'edit agmt', cmd); + + let buffer = ""; + const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], superuser: true, err: "message" }); + proc + .done(data => { this.props.reload(this.props.suffix); if (this._mounted) { this.setState({ @@ -1209,15 +1224,24 @@ export class ReplAgmts extends React.Component { 'Successfully updated replication agreement' ); }) - .fail(err => { - const errMsg = JSON.parse(err); + .fail(_ => { this.props.addNotification( "error", - `Failed to update replication agreement - ${errMsg.desc}` + `Failed to update replication agreement - ${buffer}` ); this.setState({ savingAgmt: false }); + }) + .stream(data => { + buffer += data; + const lines = buffer.split("\n"); + const last_line = lines[lines.length - 1].toLowerCase(); + if (last_line.includes("bootstrap")) { + proc.input(bootstrap_passwd + "\n", true); + } else { + proc.input(passwd + "\n", true); + } }); } @@ -1372,7 +1396,7 @@ export class ReplAgmts extends React.Component { 'success', 'Successfully deleted replication agreement'); this.setState({ - showDeleteConfirm: false, + showConfirmDeleteAgmt: false, }); }) .fail(err => { @@ -1382,7 +1406,7 @@ export class ReplAgmts extends React.Component { `Failed to delete replication agreement - ${errMsg.desc}` ); this.setState({ - showDeleteConfirm: false, + showConfirmDeleteAgmt: false, }); }); } @@ -1393,8 +1417,14 @@ export class ReplAgmts extends React.Component { 'repl-agmt', 'create', this.state.agmtName, '--suffix=' + this.props.suffix, '--host=' + this.state.agmtHost, '--port=' + this.state.agmtPort, '--bind-method=' + this.state.agmtBindMethod, '--conn-protocol=' + this.state.agmtProtocol, - '--bind-dn=' + this.state.agmtBindDN, '--bind-passwd=' + this.state.agmtBindPW + '--bind-dn=' + this.state.agmtBindDN ]; + let passwd = ""; + let bootstrap_passwd = ""; + + if (this.state.agmtBindPW != "") { + passwd = this.state.agmtBindPW; + } // Handle Schedule if (this.state.agmtSync) { @@ -1434,12 +1464,13 @@ export class ReplAgmts extends React.Component { cmd.push('--strip-list=' + this.state.agmtStripAttrs.join(' ')); } + // Handle bootstrap settings if (this.state.agmtBootstrap) { if (this.state.agmtBootstrapBindDN != "") { cmd.push('--bootstrap-bind-dn=' + this.state.agmtBootstrapBindDN); } if (this.state.agmtBootstrapBindDNPW != "") { - cmd.push('--bootstrap-bind-passwd=' + this.state.agmtBootstrapBindDNPW); + bootstrap_passwd = this.state.agmtBootstrapBindDNPW; } if (this.state.agmtBootstrapBindMethod != "") { cmd.push('--bootstrap-bind-method=' + this.state.agmtBootstrapBindMethod); @@ -1452,10 +1483,23 @@ export class ReplAgmts extends React.Component { this.setState({ savingAgmt: true }); - log_cmd('createAgmt', 'Create replication agreement', cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { + + // Update args with password prompt + if (passwd !== "") { + // Add password prompt arg + cmd.push("--bind-passwd-prompt"); + } + if (bootstrap_passwd !== "") { + // Add bootstrap password prompt arg + cmd.push("--bootstrap-bind-passwd-prompt"); + } + + log_cmd('createAgmt', 'Create agmt', cmd); + + let buffer = ""; + const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], superuser: true, err: "message" }); + proc + .done(data => { this.props.reload(this.props.suffix); if (this._mounted) { this.setState({ @@ -1471,15 +1515,24 @@ export class ReplAgmts extends React.Component { this.initAgmt(this.state.agmtName); } }) - .fail(err => { - const errMsg = JSON.parse(err); + .fail(_ => { this.props.addNotification( "error", - `Failed to create replication agreement - ${errMsg.desc}` + `Failed to create replication agreement - ${buffer}` ); this.setState({ savingAgmt: false }); + }) + .stream(data => { + buffer += data; + const lines = buffer.split("\n"); + const last_line = lines[lines.length - 1].toLowerCase(); + if (last_line.includes("bootstrap")) { + proc.input(bootstrap_passwd + "\n", true); + } else { + proc.input(passwd + "\n", true); + } }); } diff --git a/src/cockpit/389-console/src/lib/replication/replConfig.jsx b/src/cockpit/389-console/src/lib/replication/replConfig.jsx index 26c60c4d34..b96a304a07 100644 --- a/src/cockpit/389-console/src/lib/replication/replConfig.jsx +++ b/src/cockpit/389-console/src/lib/replication/replConfig.jsx @@ -1,6 +1,6 @@ import cockpit from "cockpit"; import React from "react"; -import { log_cmd, valid_dn } from "../tools.jsx"; +import { log_cmd, valid_dn, callCmdStreamPassword } from "../tools.jsx"; import { DoubleConfirmModal } from "../notifications.jsx"; import { ManagerTable } from "./replTables.jsx"; import { AddManagerModal, ChangeReplRoleModal } from "./replModals.jsx"; @@ -228,38 +228,32 @@ export class ReplConfig extends React.Component { addManagerSpinning: true }); - const cmd = [ + let cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", "replication", "create-manager", "--suffix=" + this.props.suffix, "--name=" + this.state.manager, - "--passwd=" + this.state.manager_passwd ]; - log_cmd("addManager", "Adding Replication Manager", cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.reloadConfig(this.props.suffix); - this.props.addNotification( - "success", - `Successfully added Replication Manager` - ); - this.setState({ - addManagerSpinning: false, - showAddManagerModal: false - }); + // Something changed, perform the update + const config = { + cmd: cmd, + promptArg: "", // repl manager auto prompts when passwd is missing + passwd: this.state.manager_passwd, + addNotification: this.props.addNotification, + msg: "Replication Manager", + success_msg: "Successfully added Replication Manager", + error_msg: "Failure adding Replication Manager", + state_callback: () => { + this.setState({ + addManagerSpinning: false, + showAddManagerModal: false }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.reloadConfig(this.props.suffix); - this.props.addNotification( - "error", - `Failure adding Replication Manager - ${errMsg.desc}` - ); - this.setState({ - addManagerSpinning: false, - showAddManagerModal: false - }); - }); + }, + reload_func: this.props.reloadConfig, + reload_arg: this.props.suffix, + funcName: "addManager", + funcDesc: "Adding Replication Manager" + }; + callCmdStreamPassword(config); } handleModalChange(e) { diff --git a/src/cockpit/389-console/src/lib/replication/replSuffix.jsx b/src/cockpit/389-console/src/lib/replication/replSuffix.jsx index e31155d8dd..13b4495a2f 100644 --- a/src/cockpit/389-console/src/lib/replication/replSuffix.jsx +++ b/src/cockpit/389-console/src/lib/replication/replSuffix.jsx @@ -28,7 +28,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import '@fortawesome/fontawesome-svg-core/styles.css'; import PropTypes from "prop-types"; -import { log_cmd, valid_dn } from "../tools.jsx"; +import { log_cmd, valid_dn, callCmdStreamPassword } from "../tools.jsx"; export class ReplSuffix extends React.Component { constructor (props) { @@ -213,11 +213,12 @@ export class ReplSuffix extends React.Component { 'replication', 'enable', '--suffix=' + this.props.suffix, '--role=' + this.state.enableRole ]; + let passwd = ""; if (this.state.enableBindDN != "") { cmd.push('--bind-dn=' + this.state.enableBindDN); } if (this.state.enableBindPW != "") { - cmd.push('--bind-passwd=' + this.state.enableBindPW); + passwd = this.state.enableBindPW; } if (this.state.enableBindGroupDN != "") { cmd.push('--bind-group-dn=' + this.state.enableBindGroupDN); @@ -231,24 +232,23 @@ export class ReplSuffix extends React.Component { }); this.props.disableTree(); - log_cmd('enableReplication', 'Enable replication', cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.reload(1); - this.props.addNotification( - "success", - `Successfully enabled replication for "${this.props.suffix}"` - ); - }) - .fail(err => { - this.props.reload(1); - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to enable replication for "${this.props.suffix}" - ${errMsg.desc}` - ); - }); + + + // Something changed, perform the update + const config = { + cmd: cmd, + promptArg: "--bind-passwd-prompt", + passwd: passwd, + addNotification: this.props.addNotification, + success_msg: `Successfully enabled replication for "${this.props.suffix}"`, + error_msg: `Failed to enable replication for "${this.props.suffix}"`, + state_callback: () => {}, + reload_func: this.props.reload, + reload_arg: "1", + funcName: "enableReplication", + funcDesc: "Enable replication" + }; + callCmdStreamPassword(config); } closeDisableReplModal () { @@ -259,12 +259,18 @@ export class ReplSuffix extends React.Component { disableReplication () { this.props.disableTree(); + this.setState({ + modalSpinning: true + }); const cmd = ['dsconf', '-j', 'ldapi://%2fvar%2frun%2fslapd-' + this.props.serverId + '.socket', 'replication', 'disable', '--suffix=' + this.props.suffix]; log_cmd('disableReplication', 'Disable replication', cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { this.props.reload(1); + this.setState({ + modalSpinning: false + }); this.props.addNotification( "success", `Successfully disabled replication for "${this.props.suffix}"` @@ -272,6 +278,9 @@ export class ReplSuffix extends React.Component { }) .fail(err => { this.props.reload(1); + this.setState({ + modalSpinning: false + }); const errMsg = JSON.parse(err); this.props.addNotification( "error", diff --git a/src/cockpit/389-console/src/lib/replication/winsyncAgmts.jsx b/src/cockpit/389-console/src/lib/replication/winsyncAgmts.jsx index 14d5173434..e94f292d4c 100644 --- a/src/cockpit/389-console/src/lib/replication/winsyncAgmts.jsx +++ b/src/cockpit/389-console/src/lib/replication/winsyncAgmts.jsx @@ -3,7 +3,10 @@ import React from "react"; import { DoubleConfirmModal } from "../notifications.jsx"; import { ReplAgmtTable } from "./replTables.jsx"; import { WinsyncAgmtModal } from "./replModals.jsx"; -import { log_cmd, valid_dn, valid_port, listsEqual } from "../tools.jsx"; +import { + log_cmd, valid_dn, valid_port, + listsEqual, callCmdStreamPassword +} from "../tools.jsx"; import PropTypes from "prop-types"; import { Button, @@ -776,6 +779,8 @@ export class WinsyncAgmts extends React.Component { 'repl-winsync-agmt', 'set', this.state.agmtName, '--suffix=' + this.props.suffix, ]; + let passwd = ""; + // Handle Schedule if (this.state.agmtSync) { let agmt_days = ""; @@ -860,32 +865,27 @@ export class WinsyncAgmts extends React.Component { this.setState({ savingAgmt: true }); - log_cmd('saveAgmt', 'update winsync agreement', cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.reload(this.props.suffix); - if (this._mounted) { - this.setState({ - savingAgmt: false, - showEditAgmtModal: false, - }); - } - this.props.addNotification( - 'success', - 'Successfully updated winsync agreement' - ); + + // Something changed, perform the update + const config = { + cmd: cmd, + promptArg: "--bind-passwd-prompt", + passwd: passwd, + addNotification: this.props.addNotification, + success_msg: "Successfully updated winsync agreement", + error_msg: "Failed to update winsync agreement", + state_callback: () => { + this.setState({ + savingAgmt: false, + showEditAgmtModal: false, }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to update winsync agreement - ${errMsg.desc}` - ); - this.setState({ - savingAgmt: false - }); - }); + }, + reload_func: this.props.reload, + reload_arg: this.props.suffix, + funcName: "saveAgmt", + funcDesc: "update winsync agreement" + }; + callCmdStreamPassword(config); } pokeAgmt (agmtName) { @@ -1040,7 +1040,7 @@ export class WinsyncAgmts extends React.Component { 'success', 'Successfully deleted winsync agreement'); this.setState({ - showDeleteConfirm: false, + showConfirmDeleteAgmt: false, deleteSpinning: false }); }) @@ -1051,7 +1051,7 @@ export class WinsyncAgmts extends React.Component { `Failed to delete winsync agreement - ${errMsg.desc}` ); this.setState({ - showDeleteConfirm: false, + showConfirmDeleteAgmt: false, deleteSpinning: false }); }); @@ -1063,11 +1063,13 @@ export class WinsyncAgmts extends React.Component { 'repl-winsync-agmt', 'create', this.state.agmtName, '--suffix=' + this.props.suffix, '--host=' + this.state.agmtHost, '--port=' + this.state.agmtPort, '--conn-protocol=' + this.state.agmtProtocol, - '--bind-dn=' + this.state.agmtBindDN, '--bind-passwd=' + this.state.agmtBindPW, + '--bind-dn=' + this.state.agmtBindDN, '--ds-subtree=' + this.state.agmtDSSubtree, '--win-subtree=' + this.state.agmtWinSubtree, '--win-domain=' + this.state.agmtWinDomain, '--one-way-sync=' + this.state.agmtOneWaySync ]; + let passwd = this.state.agmtBindPW; + // Handle Schedule if (this.state.agmtSync) { let agmt_days = ""; @@ -1111,35 +1113,35 @@ export class WinsyncAgmts extends React.Component { this.setState({ savingAgmt: true }); - log_cmd('createAgmt', 'Create winsync agreement', cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.reload(this.props.suffix); - if (this._mounted) { - this.setState({ - savingAgmt: false, - showCreateAgmtModal: false, - }); - } - this.props.addNotification( - 'success', - 'Successfully created winsync agreement' - ); - if (this.state.agmtInit == 'online-init') { - this.initAgmt(this.state.agmtName); - } + + // Something changed, perform the update + let ext_func = "" + if (this.state.agmtInit === 'online-init') { + ext_func = this.initAgmt; + } + + log_cmd('createAgmt', 'Create winsync agmt', cmd); + const config = { + cmd: cmd, + promptArg: "--bind-passwd-prompt", + passwd: passwd, + addNotification: this.props.addNotification, + success_msg: "Successfully created winsync agreement", + error_msg: "Failed to create winsync agreement", + state_callback: () => { + this.setState({ + savingAgmt: false, + showCreateAgmtModal: false, }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to create winsync agreement - ${errMsg.desc}` - ); - this.setState({ - savingAgmt: false - }); - }); + }, + reload_func: this.props.reload, + reload_arg: this.props.suffix, + ext_func: ext_func, + ext_arg: this.state.agmtName, + funcName: "createAgmt", + funcDesc: "Create winsync agreement" + }; + callCmdStreamPassword(config); } watchAgmtInit(agmtName, idx) { diff --git a/src/cockpit/389-console/src/lib/tools.jsx b/src/cockpit/389-console/src/lib/tools.jsx index f10e7e5e7c..13eac56a46 100644 --- a/src/cockpit/389-console/src/lib/tools.jsx +++ b/src/cockpit/389-console/src/lib/tools.jsx @@ -1,3 +1,5 @@ +import cockpit from "cockpit"; + export function searchFilter(searchFilterValue, columnsToSearch, rows) { if (searchFilterValue && rows && rows.length) { const filteredRows = []; @@ -245,3 +247,40 @@ export function validHostname(hostname) { var reHostname = new RegExp(/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/); return reHostname.exec(hostname); } + +export function callCmdStreamPassword(config) { + // Cmd will trigger CLI to prompt for password, add it via stream + let cmd = [...config.cmd]; + if (config.passwd !== "" && config.promptArg !== "") { + // Add password file arg + cmd.push(config.promptArg); + } + let buffer = ""; + + const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], superuser: true, err: "message" }); + proc + .done(data => { + config.addNotification("success", config.success_msg); + config.state_callback(); + if (config.reload_func) { + config.reload_func(config.reload_arg); + } + if (config.ext_func) { + config.ext_func(config.ext_arg); + } + }) + .fail(_ => { + config.addNotification("error", config.error_msg + ": " + buffer); + config.state_callback(); + if (config.reload_func) { + config.reload_func(config.reload_arg); + } + if (config.ext_func) { + config.ext_func(config.ext_arg); + } + }) + .stream(data => { + buffer += data; + proc.input(config.passwd + "\n", true); + }); +} diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py index 1d80b6ae2d..60dd6cd708 100644 --- a/src/lib389/lib389/cli_base/__init__.py +++ b/src/lib389/lib389/cli_base/__init__.py @@ -29,10 +29,13 @@ def _get_arg(args, msg=None, hidden=False, confirm=False): else: if hidden: if confirm: - x = getpass("%s : " % msg) - y = getpass("CONFIRM - %s : " % msg) - assert_c(x == y, "inputs do not match, aborting.") - return y + while True: + x = getpass("%s : " % msg) + y = getpass("CONFIRM - %s : " % msg) + if x != y: + log.info("Passwords do not match, try again.") + else: + return y else: return getpass("%s : " % msg) else: diff --git a/src/lib389/lib389/cli_conf/chaining.py b/src/lib389/lib389/cli_conf/chaining.py index ab7da2898c..f76e7f991d 100644 --- a/src/lib389/lib389/cli_conf/chaining.py +++ b/src/lib389/lib389/cli_conf/chaining.py @@ -1,5 +1,5 @@ # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2018 Red Hat, Inc. +# Copyright (C) 2023 Red Hat, Inc. # All rights reserved. # # License: GPL (version 3 or any later version). @@ -15,6 +15,7 @@ _get_arg, ) from lib389.cli_conf.monitor import _format_status +from lib389.utils import get_passwd_from_file arg_to_attr = { 'conn_bind_limit': 'nsbindconnectionslimit', @@ -158,6 +159,10 @@ def def_config_set(inst, basedn, log, args): def create_link(inst, basedn, log, args, warn=True): + if args.bind_pw_file is not None: + args.bind_pw = get_passwd_from_file(args.bind_pw_file) + elif args.bind_pw_prompt: + args.bind_pw = _get_arg(None, msg="Enter remote bind DN password", hidden=True, confirm=True) attrs = _args_to_attrs(args) attrs['cn'] = args.CHAIN_NAME[0] links = ChainingLinks(inst) @@ -172,6 +177,10 @@ def get_link(inst, basedn, log, args, warn=True): def edit_link(inst, basedn, log, args): chain_link = _get_link(inst, args.CHAIN_NAME[0]) + if args.bind_pw_file is not None: + args.bind_pw = get_passwd_from_file(args.bind_pw_file) + elif args.bind_pw_prompt: + args.bind_pw = _get_arg(None, msg="Enter remote bind DN password", hidden=True, confirm=True) attrs = _args_to_attrs(args) did_something = False replace_list = [] @@ -231,37 +240,58 @@ def create_parser(subparsers): def_config_set_parser = subcommands.add_parser('config-set-def', help='Set the default creation parameters for new database links') def_config_set_parser.set_defaults(func=def_config_set) - def_config_set_parser.add_argument('--conn-bind-limit', help="Sets the maximum number of BIND connections the database link establishes with the remote server") - def_config_set_parser.add_argument('--conn-op-limit', help="Sets the maximum number of LDAP connections the database link establishes with the remote server ") - def_config_set_parser.add_argument('--abandon-check-interval', help="Sets the number of seconds that pass before the server checks for abandoned operations") - def_config_set_parser.add_argument('--bind-limit', help="Sets the maximum number of concurrent bind operations per TCP connection") + def_config_set_parser.add_argument('--conn-bind-limit', + help="Sets the maximum number of BIND connections the database link establishes " + "with the remote server") + def_config_set_parser.add_argument('--conn-op-limit', + help="Sets the maximum number of LDAP connections the database link establishes " + "with the remote server ") + def_config_set_parser.add_argument('--abandon-check-interval', + help="Sets the number of seconds that pass before the server checks for " + "abandoned operations") + def_config_set_parser.add_argument('--bind-limit', + help="Sets the maximum number of concurrent bind operations per TCP connection") def_config_set_parser.add_argument('--op-limit', help="Sets the maximum number of concurrent operations allowed") def_config_set_parser.add_argument('--proxied-auth', - help="Enables or disables proxied authorization. If set to \"off\", the server executes bind for chained operations as the user set in the nsMultiplexorBindDn attribute.") - def_config_set_parser.add_argument('--conn-lifetime', help="Specifies connection lifetime in seconds. \"0\" keeps the connection open forever.") + help="Enables or disables proxied authorization. If set to \"off\", the server " + "executes bind for chained operations as the user set in the " + "nsMultiplexorBindDn attribute.") + def_config_set_parser.add_argument('--conn-lifetime', + help="Specifies connection lifetime in seconds. \"0\" keeps the connection open forever.") def_config_set_parser.add_argument('--bind-timeout', help="Sets the amount of time in seconds before a bind attempt times out") def_config_set_parser.add_argument('--return-ref', help="Enables or disables whether referrals are returned by scoped searches") - def_config_set_parser.add_argument('--check-aci', help="Enables or disables whether the server evaluates ACIs on the database link as well as the remote data server") + def_config_set_parser.add_argument('--check-aci', + help="Enables or disables whether the server evaluates ACIs on the database " + "link as well as the remote data server") def_config_set_parser.add_argument('--bind-attempts', help="Sets the number of times the server tries to bind to the remote server") def_config_set_parser.add_argument('--size-limit', help="Sets the maximum number of entries to return from a search operation") def_config_set_parser.add_argument('--time-limit', help="Sets the maximum number of seconds allowed for an operation") def_config_set_parser.add_argument('--hop-limit', - help="Sets the maximum number of times a database is allowed to chain. That is the number of times a request can be forwarded from one database link to another.") + help="Sets the maximum number of times a database is allowed to chain. That is " + "the number of times a request can be forwarded from one database link to another.") def_config_set_parser.add_argument('--response-delay', - help="Sets the maximum amount of time it can take a remote server to respond to an LDAP operation request made by a database link before an error is suspected") - def_config_set_parser.add_argument('--test-response-delay', help="Sets the duration of the test issued by the database link to check whether the remote server is responding") + help="Sets the maximum amount of time it can take a remote server to respond to " + "an LDAP operation request made by a database link before an error is suspected") + def_config_set_parser.add_argument('--test-response-delay', + help="Sets the duration of the test issued by the database link to check whether " + "the remote server is responding") def_config_set_parser.add_argument('--use-starttls', help="Configured that database links use StartTLS if set to \"on\"") - create_link_parser = subcommands.add_parser('link-create', add_help=False, conflict_handler='resolve', parents=[def_config_set_parser], - help='Create a database link to a remote server') + create_link_parser = subcommands.add_parser('link-create', add_help=False, conflict_handler='resolve', + parents=[def_config_set_parser], + help='Create a database link to a remote server') create_link_parser.set_defaults(func=create_link) create_link_parser.add_argument('CHAIN_NAME', nargs=1, help='The name of the database link') create_link_parser.add_argument('--suffix', required=True, help="Sets the suffix managed by the database link") create_link_parser.add_argument('--server-url', required=True, help="Sets the LDAP/LDAPS URL to the remote server") create_link_parser.add_argument('--bind-mech', required=True, - help="Sets the authentication method to use to authenticate to the remote server. Valid values: \"SIMPLE\" (default), \"EXTERNAL\", \"DIGEST-MD5\", or \"GSSAPI\"") - create_link_parser.add_argument('--bind-dn', required=True, help="Sets the DN of the administrative entry used to communicate with the remote server") - create_link_parser.add_argument('--bind-pw', required=True, help="Sets the password of the administrative user") + help="Sets the authentication method to use to authenticate to the remote server. " + "Valid values: \"SIMPLE\" (default), \"EXTERNAL\", \"DIGEST-MD5\", or \"GSSAPI\"") + create_link_parser.add_argument('--bind-dn', required=True, + help="Sets the DN of the administrative entry used to communicate with the remote server") + create_link_parser.add_argument('--bind-pw', help="Sets the password of the administrative user") + create_link_parser.add_argument('--bind-pw-file', help="File containing the password") + create_link_parser.add_argument('--bind-pw-prompt', action='store_true', help="Prompt for password") get_link_parser = subcommands.add_parser('link-get', help='Displays chaining database links') get_link_parser.set_defaults(func=get_link) @@ -274,9 +304,13 @@ def create_parser(subparsers): edit_link_parser.add_argument('--suffix', help="Sets the suffix managed by the database link") edit_link_parser.add_argument('--server-url', help="Sets the LDAP/LDAPS URL to the remote server") edit_link_parser.add_argument('--bind-mech', - help="Sets the authentication method to use to authenticate to the remote server: Valid values: \"SIMPLE\" (default), \"EXTERNAL\", \"DIGEST-MD5\", or \"GSSAPI\"") - edit_link_parser.add_argument('--bind-dn', help="Sets the DN of the administrative entry used to communicate with the remote server") + help="Sets the authentication method to use to authenticate to the remote server: " + "Valid values: \"SIMPLE\" (default), \"EXTERNAL\", \"DIGEST-MD5\", or \"GSSAPI\"") + edit_link_parser.add_argument('--bind-dn', + help="Sets the DN of the administrative entry used to communicate with the remote server") edit_link_parser.add_argument('--bind-pw', help="Sets the password of the administrative user") + edit_link_parser.add_argument('--bind-pw-file', help="File containing the password") + edit_link_parser.add_argument('--bind-pw-prompt', action='store_true', help="Prompt for password") delete_link_parser = subcommands.add_parser('link-delete', help='Delete a database link') delete_link_parser.set_defaults(func=delete_link) diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py index 4d634559e6..abd4b22261 100644 --- a/src/lib389/lib389/cli_conf/replication.py +++ b/src/lib389/lib389/cli_conf/replication.py @@ -1,5 +1,5 @@ # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2021 Red Hat, Inc. +# Copyright (C) 2023 Red Hat, Inc. # All rights reserved. # # License: GPL (version 3 or any later version). @@ -15,7 +15,8 @@ from getpass import getpass from lib389._constants import ReplicaRole, DSRC_HOME from lib389.cli_base.dsrc import dsrc_to_repl_monitor -from lib389.utils import is_a_dn, copy_with_permissions, ds_supports_new_changelog +from lib389.cli_base import _get_arg +from lib389.utils import is_a_dn, copy_with_permissions, ds_supports_new_changelog, get_passwd_from_file from lib389.replica import Replicas, ReplicationMonitor, BootstrapReplicationManager, Changelog5, ChangelogLDIF, Changelog from lib389.tasks import CleanAllRUVTask, AbortCleanAllRUVTask from lib389._mapped_object import DSLdapObjects @@ -85,7 +86,7 @@ def get_agmt(inst, args, winsync=False): try: agmt = agmts.get(agmt_name) except ldap.NO_SUCH_OBJECT: - raise ValueError("Could not find the agreement \"{}\" for suffix \"{}\"".format(agmt_name, args.suffix)) + raise ValueError(f"Could not find the agreement \"{agmt_name}\" for suffix \"{args.suffix}\"") return agmt @@ -146,7 +147,7 @@ def enable_replication(inst, basedn, log, args): repl_flag = '0' else: # error - unknown type - raise ValueError("Unknown replication role ({}), you must use \"supplier\", \"hub\", or \"consumer\"".format(role)) + raise ValueError(f"Unknown replication role ({role}), you must use \"supplier\", \"hub\", or \"consumer\"") # Start the propeties and update them as needed repl_properties = { @@ -202,15 +203,21 @@ def enable_replication(inst, basedn, log, args): raise ValueError("Replication is already enabled for this suffix") # Create replication manager if password was provided - if args.bind_dn and args.bind_passwd: + if args.bind_dn and (args.bind_passwd or args.bind_passwd_file or args.bind_passwd_prompt): rdn = args.bind_dn.split(",", 1)[0] rdn_attr, rdn_val = rdn.split("=", 1) manager = BootstrapReplicationManager(inst, dn=args.bind_dn, rdn_attr=rdn_attr) + if args.bind_passwd_file is not None: + passwd = get_passwd_from_file(args.bind_passwd_file) + elif args.bind_passwd_prompt: + passwd = _get_arg(None, msg="Enter Replication Manager password", hidden=True, confirm=True) + else: + passwd = args.bind_passwd try: manager.create(properties={ 'cn': rdn_val, 'uid': rdn_val, - 'userPassword': args.bind_passwd + 'userPassword': passwd }) except ldap.ALREADY_EXISTS: # Already there, but could have different password. Delete and recreate @@ -218,7 +225,7 @@ def enable_replication(inst, basedn, log, args): manager.create(properties={ 'cn': rdn_val, 'uid': rdn_val, - 'userPassword': args.bind_passwd + 'userPassword': passwd }) except ldap.NO_SUCH_OBJECT: # Invalid Entry @@ -227,7 +234,7 @@ def enable_replication(inst, basedn, log, args): # Some other bad error raise ValueError("Failed to create replication manager entry: " + str(e)) - log.info("Replication successfully enabled for \"{}\"".format(repl_root)) + log.info(f"Replication successfully enabled for \"{repl_root}\"") def disable_replication(inst, basedn, log, args): @@ -236,8 +243,8 @@ def disable_replication(inst, basedn, log, args): replica = replicas.get(args.suffix) replica.delete() except ldap.NO_SUCH_OBJECT: - raise ValueError("Backend \"{}\" is not enabled for replication".format(args.suffix)) - log.info("Replication disabled for \"{}\"".format(args.suffix)) + raise ValueError(f"Backend \"{args.suffix}\" is not enabled for replication") + log.info(f"Replication disabled for \"{args.suffix}\"") def promote_replica(inst, basedn, log, args): @@ -252,10 +259,10 @@ def promote_replica(inst, basedn, log, args): elif role == 'hub': newrole = ReplicaRole.HUB else: - raise ValueError("Invalid role ({}), you must use either \"supplier\" or \"hub\"".format(role)) + raise ValueError(f"Invalid role ({role}), you must use either \"supplier\" or \"hub\"") replica.promote(newrole, binddn=args.bind_dn, binddn_group=args.bind_group_dn, rid=args.replica_id) - log.info("Successfully promoted replica to \"{}\"".format(role)) + log.info(f"Successfully promoted replica to \"{role}\"") def demote_replica(inst, basedn, log, args): @@ -268,10 +275,10 @@ def demote_replica(inst, basedn, log, args): elif role == 'consumer': newrole = ReplicaRole.CONSUMER else: - raise ValueError("Invalid role ({}), you must use either \"hub\" or \"consumer\"".format(role)) + raise ValueError(f"Invalid role ({role}), you must use either \"hub\" or \"consumer\"") replica.demote(newrole) - log.info("Successfully demoted replica to \"{}\"".format(role)) + log.info(f"Successfully demoted replica to \"{role}\"") def list_suffixes(inst, basedn, log, args): @@ -293,7 +300,13 @@ def list_suffixes(inst, basedn, log, args): def get_repl_status(inst, basedn, log, args): replicas = Replicas(inst) replica = replicas.get(args.suffix) - status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd) + if args.bind_passwd_file is not None: + passwd = get_passwd_from_file(args.bind_passwd_file) + elif args.bind_passwd_prompt: + passwd = _get_arg(None, msg=f"Enter password for ({args.bind_dn})", hidden=True, confirm=True) + else: + passwd = args.bind_passwd + status = replica.status(binddn=args.bind_dn, bindpw=passwd) if args.json: log.info(json.dumps({"type": "list", "items": status}, indent=4)) else: @@ -304,7 +317,14 @@ def get_repl_status(inst, basedn, log, args): def get_repl_winsync_status(inst, basedn, log, args): replicas = Replicas(inst) replica = replicas.get(args.suffix) - status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, winsync=True) + if args.bind_passwd_file is not None: + passwd = get_passwd_from_file(args.bind_passwd_file) + elif args.bind_passwd_prompt: + passwd = _get_arg(None, msg=f"Enter password for ({args.bind_dn})", hidden=True, confirm=True) + else: + passwd = args.bind_passwd + + status = replica.status(binddn=args.bind_dn, bindpw=passwd, winsync=True) if args.json: log.info(json.dumps({"type": "list", "items": status}, indent=4)) else: @@ -390,7 +410,7 @@ def get_credentials(host, port): if connections: for connection_str in connections: connection = connection_str.split(":") - if (len(connection) != 4 or not all([len(str) > 0 for str in connection])): + if len(connection) != 4 or not all([len(str) > 0 for str in connection]): raise ValueError(f"Please, fill in all Credential details. It should be host:port:binddn:bindpw") host_regex = connection[0] port_regex = connection[1] @@ -452,7 +472,7 @@ def get_credentials(host, port): for replica in report_data: if replica["replica_status"].startswith("Unreachable") or \ - replica["replica_status"].startswith("Unavailable"): + replica["replica_status"].startswith("Unavailable"): status = replica["replica_status"] if not args.json: log.info(f"Replica Status: {status}\n") @@ -477,6 +497,7 @@ def get_credentials(host, port): if args.json: log.info(json.dumps({"type": "list", "items": report_items}, indent=4)) + # This subcommand is available when 'not ds_supports_new_changelog' def create_cl(inst, basedn, log, args): cl = Changelog5(inst) @@ -528,6 +549,7 @@ def get_cl(inst, basedn, log, args): else: log.info(cl.display()) + # This subcommand is available when 'ds_supports_new_changelog' # that means there is a changelog config entry per backend (aka suffix) def set_per_backend_cl(inst, basedn, log, args): @@ -561,6 +583,7 @@ def set_per_backend_cl(inst, basedn, log, args): log.info("Successfully updated replication changelog") + # This subcommand is available when 'ds_supports_new_changelog' # that means there is a changelog config entry per backend (aka suffix) def get_per_backend_cl(inst, basedn, log, args): @@ -575,7 +598,6 @@ def get_per_backend_cl(inst, basedn, log, args): def create_repl_manager(inst, basedn, log, args): manager_name = "replication manager" repl_manager_password = "" - repl_manager_password_confirm = "" if args.name: manager_name = args.name @@ -588,24 +610,16 @@ def create_repl_manager(inst, basedn, log, args): if manager_attr.lower() not in ['cn', 'uid']: raise ValueError(f'The RDN attribute "{manager_attr}" is not allowed, you must use "cn" or "uid"') else: - manager_dn = "cn={},cn=config".format(manager_name) + manager_dn = f"cn={manager_name},cn=config" manager_attr = "cn" - if args.passwd: + if args.passwd is not None: repl_manager_password = args.passwd - else: - # Prompt for password - while 1: - while repl_manager_password == "": - repl_manager_password = getpass("Enter replication manager password: ") - while repl_manager_password_confirm == "": - repl_manager_password_confirm = getpass("Confirm replication manager password: ") - if repl_manager_password_confirm == repl_manager_password: - break - else: - log.info("Passwords do not match!\n") - repl_manager_password = "" - repl_manager_password_confirm = "" + elif args.passwd_file is not None: + repl_manager_password = get_passwd_from_file(args.bind_passwd_file) + elif repl_manager_password == "": + repl_manager_password = _get_arg(None, msg=f"Enter replication manager password for \"{manager_dn}\"", + hidden=True, confirm=True) manager = BootstrapReplicationManager(inst, dn=manager_dn, rdn_attr=manager_attr) try: @@ -624,7 +638,7 @@ def create_repl_manager(inst, basedn, log, args): pass log.info("Successfully created replication manager: " + manager_dn) except ldap.ALREADY_EXISTS: - log.info("Replication Manager ({}) already exists, recreating it...".format(manager_dn)) + log.info(f"Replication Manager ({manager_dn}) already exists, recreating it...") # Already there, but could have different password. Delete and recreate manager.delete() manager.create(properties={ @@ -652,7 +666,7 @@ def del_repl_manager(inst, basedn, log, args): if is_a_dn(args.name): manager_dn = args.name else: - manager_dn = "cn={},cn=config".format(args.name) + manager_dn = f"cn={args.name},cn=config" manager = BootstrapReplicationManager(inst, dn=manager_dn) try: @@ -737,7 +751,13 @@ def add_agmt(inst, basedn, log, args): if not is_a_dn(args.bind_dn): raise ValueError("The replica bind DN is not a valid DN") properties['nsDS5ReplicaBindDN'] = args.bind_dn - if args.bind_passwd is not None: + if args.bind_passwd_file is not None: + passwd = get_passwd_from_file(args.bind_passwd_file) + properties['nsDS5ReplicaCredentials'] = passwd + elif args.bind_passwd_prompt: + passwd = _get_arg(None, msg="Enter password", hidden=True, confirm=True) + properties['nsDS5ReplicaCredentials'] = passwd + elif args.bind_passwd is not None: properties['nsDS5ReplicaCredentials'] = args.bind_passwd if args.schedule is not None: properties['nsds5replicaupdateschedule'] = args.schedule @@ -753,7 +773,14 @@ def add_agmt(inst, basedn, log, args): if not is_a_dn(args.bootstrap_bind_dn): raise ValueError("The replica bootstrap bind DN is not a valid DN") properties['nsDS5ReplicaBootstrapBindDN'] = args.bootstrap_bind_dn - if args.bootstrap_bind_passwd is not None: + + if args.bootstrap_bind_passwd_file is not None: + passwd = get_passwd_from_file(args.bootstrap_bind_passwd_file) + properties['nsDS5ReplicaBootstrapCredentials'] = passwd + elif args.bootstrap_bind_passwd_prompt: + passwd = _get_arg(None, msg="Enter bootstrap password", hidden=True, confirm=True) + properties['nsDS5ReplicaBootstrapCredentials'] = passwd + elif args.bootstrap_bind_passwd is not None: properties['nsDS5ReplicaBootstrapCredentials'] = args.bootstrap_bind_passwd if args.bootstrap_bind_method is not None: bs_bind_method = args.bootstrap_bind_method.lower() @@ -767,8 +794,12 @@ def add_agmt(inst, basedn, log, args): properties['nsDS5ReplicaBootstrapTransportInfo'] = args.bootstrap_conn_protocol # We do need the bind dn and credentials for 'simple' bind method - if (bind_method == 'simple') and (args.bind_dn is None or args.bind_passwd is None): - raise ValueError("You need to set the bind dn (--bind-dn) and the password (--bind-passwd) for bind method ({})".format(bind_method)) + if (bind_method == 'simple') and (args.bind_dn is None or + (args.bind_passwd is None and + args.bind_passwd_file is None and + args.bind_passwd_prompt is False)): + raise ValueError(f"You need to set the bind dn (--bind-dn) and the password (--bind-passwd or -" + f"-bind-passwd-file or --bind-passwd-prompt) for bind method ({bind_method})") # Create the agmt try: @@ -776,7 +807,7 @@ def add_agmt(inst, basedn, log, args): except ldap.ALREADY_EXISTS: raise ValueError("A replication agreement with the same name already exists") - log.info("Successfully created replication agreement \"{}\"".format(get_agmt_name(args))) + log.info(f"Successfully created replication agreement \"{get_agmt_name(args)}\"") if args.init: init_agmt(inst, basedn, log, args) @@ -823,6 +854,10 @@ def check_init_agmt(inst, basedn, log, args): def set_agmt(inst, basedn, log, args): agmt = get_agmt(inst, args) + if args.bind_passwd_prompt: + args.bind_passwd = _get_arg(None, msg="Enter password", hidden=True, confirm=True) + if args.bootstrap_bind_passwd_prompt: + args.bootstrap_bind_passwd = _get_arg(None, msg="Enter bootstrap password", hidden=True, confirm=True) attrs = _args_to_attrs(args) modlist = [] did_something = False @@ -873,10 +908,10 @@ def poke_agmt(inst, basedn, log, args): def get_agmt_status(inst, basedn, log, args): agmt = get_agmt(inst, args) - if args.bind_dn is not None and args.bind_passwd is None: - args.bind_passwd = "" - while args.bind_passwd == "": - args.bind_passwd = getpass("Enter password for \"{}\": ".format(args.bind_dn)) + if args.bind_passwd_file is not None: + args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) + if (args.bind_dn is not None and args.bind_passwd is None) or args.bind_passwd_prompt: + args.bind_passwd = _get_arg(None, msg=f"Enter password for \"{args.bind_dn}\"", hidden=True, confirm=True) status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd) log.info(status) @@ -917,6 +952,13 @@ def add_winsync_agmt(inst, basedn, log, args): if not is_a_dn(args.bind_dn): raise ValueError("The replica bind DN is not a valid DN") + if args.bind_passwd_file is not None: + passwd = get_passwd_from_file(args.bind_passwd_file) + if args.bind_passwd_prompt: + passwd = _get_arg(None, msg="Enter password", hidden=True, confirm=True) + else: + passwd = args.bind_passwd + # Required properties properties = { 'cn': get_agmt_name(args), @@ -926,7 +968,7 @@ def add_winsync_agmt(inst, basedn, log, args): 'nsDS5ReplicaPort': args.port, 'nsDS5ReplicaTransportInfo': args.conn_protocol, 'nsDS5ReplicaBindDN': args.bind_dn, - 'nsDS5ReplicaCredentials': args.bind_passwd, + 'nsDS5ReplicaCredentials': passwd, 'nsds7windowsreplicasubtree': args.win_subtree, 'nsds7directoryreplicasubtree': args.ds_subtree, 'nsds7windowsDomain': args.win_domain, @@ -954,13 +996,17 @@ def add_winsync_agmt(inst, basedn, log, args): if args.flatten_tree is True: properties['winsyncflattentree'] = "on" + # We do need the bind dn and credentials for 'simple' bind method + if passwd is None: + raise ValueError("You need to provide a password (--bind-passwd, --bind-passwd-file, or --bind-passwd-prompt)") + # Create the agmt try: agmts.create(properties=properties) except ldap.ALREADY_EXISTS: raise ValueError("A replication agreement with the same name already exists") - log.info("Successfully created winsync replication agreement \"{}\"".format(get_agmt_name(args))) + log.info(f"Successfully created winsync replication agreement \"{get_agmt_name(args)}\"") if args.init: init_winsync_agmt(inst, basedn, log, args) @@ -973,7 +1019,8 @@ def delete_winsync_agmt(inst, basedn, log, args): def set_winsync_agmt(inst, basedn, log, args): agmt = get_agmt(inst, args, winsync=True) - + if args.bind_passwd_prompt: + args.bind_passwd = _get_arg(None, msg="Enter password", hidden=True, confirm=True) attrs = _args_to_attrs(args) modlist = [] did_something = False @@ -1235,7 +1282,11 @@ def create_parser(subparsers): repl_enable_parser.add_argument('--replica-id', help="Sets the replication identifier for a \"supplier\". Values range from 1 - 65534") repl_enable_parser.add_argument('--bind-group-dn', help="Sets a group entry DN containing members that are \"bind/supplier\" DNs") repl_enable_parser.add_argument('--bind-dn', help="Sets the bind or supplier DN that can make replication updates") - repl_enable_parser.add_argument('--bind-passwd', help="Sets the password for replication manager (--bind-dn). This will create the manager entry if a value is set") + repl_enable_parser.add_argument('--bind-passwd', + help="Sets the password for replication manager (--bind-dn). This will create the " + "manager entry if a value is set") + repl_enable_parser.add_argument('--bind-passwd-file', help="File containing the password") + repl_enable_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") repl_disable_parser = repl_subcommands.add_parser('disable', help='Disable replication for a suffix') repl_disable_parser.set_defaults(func=disable_replication) @@ -1253,12 +1304,17 @@ def create_parser(subparsers): repl_status_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication suffix") repl_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer") repl_status_parser.add_argument('--bind-passwd', help="Sets the password for the bind DN") + repl_status_parser.add_argument('--bind-passwd-file', help="File containing the password") + repl_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") - repl_winsync_status_parser = repl_subcommands.add_parser('winsync-status', help='Display the current status of all the replication agreements') + repl_winsync_status_parser = repl_subcommands.add_parser('winsync-status', help='Display the current status of all ' + 'the replication agreements') repl_winsync_status_parser.set_defaults(func=get_repl_winsync_status) repl_winsync_status_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication suffix") repl_winsync_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer") repl_winsync_status_parser.add_argument('--bind-passwd', help="Sets the password of the bind DN") + repl_winsync_status_parser.add_argument('--bind-passwd-file', help="File containing the password") + repl_winsync_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") repl_promote_parser = repl_subcommands.add_parser('promote', help='Promote a replica to a hub or supplier') repl_promote_parser.set_defaults(func=promote_replica) @@ -1273,7 +1329,9 @@ def create_parser(subparsers): repl_add_manager_parser.add_argument('--name', help="Sets the name of the new replication manager entry.For example, " + "if the name is \"replication manager\" then the new manager " + "entry's DN would be \"cn=replication manager,cn=config\".") - repl_add_manager_parser.add_argument('--passwd', help="Sets the password for replication manager. If not provided, you will be prompted for the password") + repl_add_manager_parser.add_argument('--passwd', help="Sets the password for replication manager. If not provided, " + "you will be prompted for the password") + repl_add_manager_parser.add_argument('--passwd-file', help="File containing the password") repl_add_manager_parser.add_argument('--suffix', help='The DN of the replication suffix whose replication ' + 'configuration you want to add this new manager to (OPTIONAL)') @@ -1298,8 +1356,12 @@ def create_parser(subparsers): repl_set_per_backend_cl.add_argument('--max-entries', help="Sets the maximum number of entries to get in the replication changelog") repl_set_per_backend_cl.add_argument('--max-age', help="Set the maximum age of a replication changelog entry") repl_set_per_backend_cl.add_argument('--trim-interval', help="Sets the interval to check if the replication changelog can be trimmed") - repl_set_per_backend_cl.add_argument('--encrypt', action='store_true', help="Sets the replication changelog to use encryption. You must export and import the changelog after setting this.") - repl_set_per_backend_cl.add_argument('--disable-encrypt', action='store_true', help="Sets the replication changelog to not use encryption. You must export and import the changelog after setting this.") + repl_set_per_backend_cl.add_argument('--encrypt', action='store_true', + help="Sets the replication changelog to use encryption. You must export and " + "import the changelog after setting this.") + repl_set_per_backend_cl.add_argument('--disable-encrypt', action='store_true', + help="Sets the replication changelog to not use encryption. You must export " + "and import the changelog after setting this.") repl_get_per_backend_cl = repl_subcommands.add_parser('get-changelog', help='Display replication changelog attributes') repl_get_per_backend_cl.set_defaults(func=get_per_backend_cl) @@ -1430,6 +1492,8 @@ def create_parser(subparsers): agmt_status_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication suffix") agmt_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer") agmt_status_parser.add_argument('--bind-passwd', help="Sets the password for the bind DN") + agmt_status_parser.add_argument('--bind-passwd-file', help="File containing the password") + agmt_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") # Delete agmt_del_parser = agmt_subcommands.add_parser('delete', help='Delete replication agreement') @@ -1447,7 +1511,10 @@ def create_parser(subparsers): agmt_add_parser.add_argument('--conn-protocol', required=True, help="Sets the replication connection protocol: LDAP, LDAPS, or StartTLS") agmt_add_parser.add_argument('--bind-dn', help="Sets the bind DN the agreement uses to authenticate to the replica") agmt_add_parser.add_argument('--bind-passwd', help="Sets the credentials for the bind DN") - agmt_add_parser.add_argument('--bind-method', required=True, help="Sets the bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"") + agmt_add_parser.add_argument('--bind-passwd-file', help="File containing the password") + agmt_add_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") + agmt_add_parser.add_argument('--bind-method', required=True, + help="Sets the bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"") agmt_add_parser.add_argument('--frac-list', help="Sets the list of attributes to NOT replicate to the consumer during incremental updates") agmt_add_parser.add_argument('--frac-list-total', help="Sets the list of attributes to NOT replicate during a total initialization") agmt_add_parser.add_argument('--strip-list', help="Sets a list of attributes that are removed from updates only if the event " @@ -1462,11 +1529,20 @@ def create_parser(subparsers): "a consumer sends back a busy response before making another " "attempt to acquire access.") agmt_add_parser.add_argument('--session-pause-time', help="Sets the amount of time in seconds a supplier should wait between update sessions.") - agmt_add_parser.add_argument('--flow-control-window', help="Sets the maximum number of entries and updates sent by a supplier, which are not acknowledged by the consumer.") - agmt_add_parser.add_argument('--flow-control-pause', help="Sets the time in milliseconds to pause after reaching the number of entries and updates set in \"--flow-control-window\"") - agmt_add_parser.add_argument('--bootstrap-bind-dn', help="Sets an optional bind DN the agreement can use to bootstrap initialization when bind groups are being used") + agmt_add_parser.add_argument('--flow-control-window', + help="Sets the maximum number of entries and updates sent by a supplier, which are not " + "acknowledged by the consumer.") + agmt_add_parser.add_argument('--flow-control-pause', + help="Sets the time in milliseconds to pause after reaching the number of entries and " + "updates set in \"--flow-control-window\"") + agmt_add_parser.add_argument('--bootstrap-bind-dn', + help="Sets an optional bind DN the agreement can use to bootstrap initialization when " + "bind groups are being used") agmt_add_parser.add_argument('--bootstrap-bind-passwd', help="Sets the bootstrap credentials for the bind DN") - agmt_add_parser.add_argument('--bootstrap-conn-protocol', help="Sets the replication bootstrap connection protocol: LDAP, LDAPS, or StartTLS") + agmt_add_parser.add_argument('--bootstrap-bind-passwd-file', help="File containing the password") + agmt_add_parser.add_argument('--bootstrap-bind-passwd-prompt', action='store_true', help="File containing the password") + agmt_add_parser.add_argument('--bootstrap-conn-protocol', + help="Sets the replication bootstrap connection protocol: LDAP, LDAPS, or StartTLS") agmt_add_parser.add_argument('--bootstrap-bind-method', help="Sets the bind method: \"SIMPLE\", or \"SSLCLIENTAUTH\"") agmt_add_parser.add_argument('--init', action='store_true', default=False, help="Initializes the agreement after creating it") @@ -1480,6 +1556,8 @@ def create_parser(subparsers): agmt_set_parser.add_argument('--conn-protocol', help="Sets the replication connection protocol: LDAP, LDAPS, or StartTLS") agmt_set_parser.add_argument('--bind-dn', help="Sets the Bind DN the agreement uses to authenticate to the replica") agmt_set_parser.add_argument('--bind-passwd', help="Sets the credentials for the bind DN") + agmt_set_parser.add_argument('--bind-passwd-file', help="File containing the password") + agmt_set_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") agmt_set_parser.add_argument('--bind-method', help="Sets the bind method: \"SIMPLE\", \"SSLCLIENTAUTH\", \"SASL/DIGEST\", or \"SASL/GSSAPI\"") agmt_set_parser.add_argument('--frac-list', help="Sets a list of attributes to NOT replicate to the consumer during incremental updates") agmt_set_parser.add_argument('--frac-list-total', help="Sets a list of attributes to NOT replicate during a total initialization") @@ -1493,12 +1571,22 @@ def create_parser(subparsers): "the consumer is not ready before resending data") agmt_set_parser.add_argument('--busy-wait-time', help="Sets the amount of time in seconds a supplier should wait after " "a consumer sends back a busy response before making another attempt to acquire access.") - agmt_set_parser.add_argument('--session-pause-time', help="Sets the amount of time in seconds a supplier should wait between update sessions.") - agmt_set_parser.add_argument('--flow-control-window', help="Sets the maximum number of entries and updates sent by a supplier, which are not acknowledged by the consumer.") - agmt_set_parser.add_argument('--flow-control-pause', help="Sets the time in milliseconds to pause after reaching the number of entries and updates set in \"--flow-control-window\"") - agmt_set_parser.add_argument('--bootstrap-bind-dn', help="Sets an optional bind DN the agreement can use to bootstrap initialization when bind groups are being used") + agmt_set_parser.add_argument('--session-pause-time', + help="Sets the amount of time in seconds a supplier should wait between update sessions.") + agmt_set_parser.add_argument('--flow-control-window', + help="Sets the maximum number of entries and updates sent by a supplier, which are not " + "acknowledged by the consumer.") + agmt_set_parser.add_argument('--flow-control-pause', + help="Sets the time in milliseconds to pause after reaching the number of entries and " + "updates set in \"--flow-control-window\"") + agmt_set_parser.add_argument('--bootstrap-bind-dn', + help="Sets an optional bind DN the agreement can use to bootstrap initialization when " + "bind groups are being used") agmt_set_parser.add_argument('--bootstrap-bind-passwd', help="sets the bootstrap credentials for the bind DN") - agmt_set_parser.add_argument('--bootstrap-conn-protocol', help="Sets the replication bootstrap connection protocol: LDAP, LDAPS, or StartTLS") + agmt_set_parser.add_argument('--bootstrap-bind-passwd-file', help="File containing the password") + agmt_set_parser.add_argument('--bootstrap-bind-passwd-prompt', action='store_true', help="Prompt for password") + agmt_set_parser.add_argument('--bootstrap-conn-protocol', + help="Sets the replication bootstrap connection protocol: LDAP, LDAPS, or StartTLS") agmt_set_parser.add_argument('--bootstrap-bind-method', help="Sets the bind method: \"SIMPLE\", or \"SSLCLIENTAUTH\"") # Get @@ -1568,10 +1656,15 @@ def create_parser(subparsers): winsync_agmt_add_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication winsync suffix") winsync_agmt_add_parser.add_argument('--host', required=True, help="Sets the hostname of the AD server") winsync_agmt_add_parser.add_argument('--port', required=True, help="Sets the port number of the AD server") - winsync_agmt_add_parser.add_argument('--conn-protocol', required=True, help="Sets the replication winsync connection protocol: LDAP, LDAPS, or StartTLS") - winsync_agmt_add_parser.add_argument('--bind-dn', required=True, help="Sets the bind DN the agreement uses to authenticate to the AD Server") - winsync_agmt_add_parser.add_argument('--bind-passwd', required=True, help="Sets the credentials for the Bind DN") - winsync_agmt_add_parser.add_argument('--frac-list', help="Sets a list of attributes to NOT replicate to the consumer during incremental updates") + winsync_agmt_add_parser.add_argument('--conn-protocol', required=True, + help="Sets the replication winsync connection protocol: LDAP, LDAPS, or StartTLS") + winsync_agmt_add_parser.add_argument('--bind-dn', required=True, + help="Sets the bind DN the agreement uses to authenticate to the AD Server") + winsync_agmt_add_parser.add_argument('--bind-passwd', help="Sets the credentials for the Bind DN") + winsync_agmt_add_parser.add_argument('--bind-passwd-file', help="File containing the password") + winsync_agmt_add_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") + winsync_agmt_add_parser.add_argument('--frac-list', + help="Sets a list of attributes to NOT replicate to the consumer during incremental updates") winsync_agmt_add_parser.add_argument('--schedule', help="Sets the replication update schedule") winsync_agmt_add_parser.add_argument('--win-subtree', required=True, help="Sets the suffix of the AD Server") winsync_agmt_add_parser.add_argument('--ds-subtree', required=True, help="Sets the Directory Server suffix") @@ -1579,16 +1672,27 @@ def create_parser(subparsers): winsync_agmt_add_parser.add_argument('--sync-users', help="Synchronizes users between AD and DS") winsync_agmt_add_parser.add_argument('--sync-groups', help="Synchronizes groups between AD and DS") winsync_agmt_add_parser.add_argument('--sync-interval', help="Sets the interval that DS checks AD for changes in entries") - winsync_agmt_add_parser.add_argument('--one-way-sync', help="Sets which direction to perform synchronization: \"toWindows\", or \"fromWindows\,. By default sync occurs in both directions.") - winsync_agmt_add_parser.add_argument('--move-action', help="Sets instructions on how to handle moved or deleted entries: \"none\", \"unsync\", or \"delete\"") + winsync_agmt_add_parser.add_argument('--one-way-sync', + help="Sets which direction to perform synchronization: \"toWindows\", or " + "\"fromWindows\,. By default sync occurs in both directions.") + winsync_agmt_add_parser.add_argument('--move-action', + help="Sets instructions on how to handle moved or deleted entries: " + "\"none\", \"unsync\", or \"delete\"") winsync_agmt_add_parser.add_argument('--win-filter', help="Sets a custom filter for finding users in AD Server") winsync_agmt_add_parser.add_argument('--ds-filter', help="Sets a custom filter for finding AD users in DS") winsync_agmt_add_parser.add_argument('--subtree-pair', help="Sets the subtree pair: :") winsync_agmt_add_parser.add_argument('--conn-timeout', help="Sets the timeout used for replicaton connections") winsync_agmt_add_parser.add_argument('--busy-wait-time', help="Sets the amount of time in seconds a supplier should wait after " "a consumer sends back a busy response before making another attempt to acquire access") - winsync_agmt_add_parser.add_argument('--session-pause-time', help="Sets the amount of time in seconds a supplier should wait between update sessions") - winsync_agmt_add_parser.add_argument('--flatten-tree', action='store_true', default=False, help="By default, the tree structure of AD is preserved into 389. This MAY cause replication to fail in some cases, as you may need to create missing OU's to recreate the same treestructure. This setting when enabled, removes the tree structure of AD and flattens all entries into the ds-subtree. This does NOT affect or change the tree structure of the AD directory.") + winsync_agmt_add_parser.add_argument('--session-pause-time', + help="Sets the amount of time in seconds a supplier should wait between update sessions") + winsync_agmt_add_parser.add_argument('--flatten-tree', action='store_true', default=False, + help="By default, the tree structure of AD is preserved into 389. This MAY " + "cause replication to fail in some cases, as you may need to create " + "missing OU's to recreate the same treestructure. This setting when " + "enabled, removes the tree structure of AD and flattens all entries " + "into the ds-subtree. This does NOT affect or change the tree structure " + "of the AD directory.") winsync_agmt_add_parser.add_argument('--init', action='store_true', default=False, help="Initializes the agreement after creating it") # Set - Note can not use add's parent args because for "set" there are no "required=True" args @@ -1601,6 +1705,8 @@ def create_parser(subparsers): winsync_agmt_set_parser.add_argument('--conn-protocol', help="Sets the replication winsync connection protocol: LDAP, LDAPS, or StartTLS") winsync_agmt_set_parser.add_argument('--bind-dn', help="Sets the bind DN the agreement uses to authenticate to the AD Server") winsync_agmt_set_parser.add_argument('--bind-passwd', help="Sets the credentials for the Bind DN") + winsync_agmt_set_parser.add_argument('--bind-passwd-file', help="File containing the password") + winsync_agmt_set_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") winsync_agmt_set_parser.add_argument('--frac-list', help="Sets a list of attributes to NOT replicate to the consumer during incremental updates") winsync_agmt_set_parser.add_argument('--schedule', help="Sets the replication update schedule") winsync_agmt_set_parser.add_argument('--win-subtree', help="Sets the suffix of the AD Server") @@ -1609,15 +1715,20 @@ def create_parser(subparsers): winsync_agmt_set_parser.add_argument('--sync-users', help="Synchronizes users between AD and DS") winsync_agmt_set_parser.add_argument('--sync-groups', help="Synchronizes groups between AD and DS") winsync_agmt_set_parser.add_argument('--sync-interval', help="Sets the interval that DS checks AD for changes in entries") - winsync_agmt_set_parser.add_argument('--one-way-sync', help="Sets which direction to perform synchronization: \"toWindows\", or \"fromWindows\". By default sync occurs in both directions.") - winsync_agmt_set_parser.add_argument('--move-action', help="Sets instructions on how to handle moved or deleted entries: \"none\", \"unsync\", or \"delete\"") + winsync_agmt_set_parser.add_argument('--one-way-sync', + help="Sets which direction to perform synchronization: \"toWindows\", or " + "\"fromWindows\". By default sync occurs in both directions.") + winsync_agmt_set_parser.add_argument('--move-action', + help="Sets instructions on how to handle moved or deleted entries: \"none\", " + "\"unsync\", or \"delete\"") winsync_agmt_set_parser.add_argument('--win-filter', help="Sets a custom filter for finding users in AD Server") winsync_agmt_set_parser.add_argument('--ds-filter', help="Sets a custom filter for finding AD users in DS") winsync_agmt_set_parser.add_argument('--subtree-pair', help="Sets the subtree pair: :") winsync_agmt_set_parser.add_argument('--conn-timeout', help="Sets the timeout used for replicaton connections") winsync_agmt_set_parser.add_argument('--busy-wait-time', help="Sets the amount of time in seconds a supplier should wait after " "a consumer sends back a busy response before making another attempt to acquire access") - winsync_agmt_set_parser.add_argument('--session-pause-time', help="Sets the amount of time in seconds a supplier should wait between update sessions") + winsync_agmt_set_parser.add_argument('--session-pause-time', + help="Sets the amount of time in seconds a supplier should wait between update sessions") # Get winsync_agmt_get_parser = winsync_agmt_subcommands.add_parser('get', help='Display replication configuration') diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py index 345bf10fb4..baeb7b8953 100644 --- a/src/lib389/lib389/utils.py +++ b/src/lib389/lib389/utils.py @@ -1803,3 +1803,11 @@ def cert_is_ca(cert_file_name): # This is a CA cert return True + + +def get_passwd_from_file(passwd_file): + if os.path.exists(passwd_file): + with open(passwd_file, 'r') as f: + passwd = f.readline().strip() + return passwd + raise ValueError(f"The password file '{passwd_file}' does not exist, or can not be read.")