diff --git a/dirsrvtests/tests/suites/config/compact_test.py b/dirsrvtests/tests/suites/config/compact_test.py index a67a7dfff7..317258d0eb 100644 --- a/dirsrvtests/tests/suites/config/compact_test.py +++ b/dirsrvtests/tests/suites/config/compact_test.py @@ -66,6 +66,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: @@ -74,6 +81,7 @@ def test_compaction_interval_and_time(topo): minute = "0" + str(current_minute) else: minute = str(current_minute) + compact_time = hour + ":" + minute # Configure changelog compaction @@ -86,11 +94,11 @@ def test_compaction_interval_and_time(topo): inst.deleteErrorLogs() # Check compaction occurred as expected - time.sleep(60) - assert not inst.searchErrorsLog("compacting replication changelogs") + time.sleep(45) + assert not inst.searchErrorsLog("Compacting databases") - time.sleep(61) - assert inst.searchErrorsLog("compacting replication changelogs") + 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 38fb23c903..a05e1ee764 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 17320ac96a..8a919da989 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). @@ -16,7 +16,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 +from lib389.cli_base import _get_arg +from lib389.utils import is_a_dn, copy_with_permissions, get_passwd_from_file from lib389.replica import Replicas, ReplicationMonitor, BootstrapReplicationManager, Changelog5, ChangelogLDIF from lib389.tasks import CleanAllRUVTask, AbortCleanAllRUVTask from lib389._mapped_object import DSLdapObjects @@ -88,7 +89,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 @@ -149,7 +150,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 = { @@ -204,15 +205,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 @@ -220,7 +227,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 @@ -229,7 +236,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): @@ -238,8 +245,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): @@ -254,10 +261,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): @@ -270,10 +277,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): @@ -295,7 +302,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: @@ -306,7 +319,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: @@ -392,7 +412,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] @@ -530,7 +550,6 @@ def get_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 @@ -543,24 +562,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: @@ -579,7 +590,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={ @@ -607,7 +618,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: @@ -692,7 +703,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 @@ -708,7 +725,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() @@ -722,8 +746,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: @@ -731,7 +759,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) @@ -778,6 +806,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 @@ -828,10 +860,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) @@ -872,6 +904,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), @@ -881,7 +920,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, @@ -907,13 +946,17 @@ def add_winsync_agmt(inst, basedn, log, args): if frac_list is not None: properties['nsds5replicatedattributelist'] = frac_list + # 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) @@ -926,7 +969,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 @@ -1156,7 +1200,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) @@ -1174,12 +1222,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) @@ -1194,7 +1247,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)') @@ -1215,7 +1270,7 @@ def create_parser(subparsers): repl_create_cl = repl_subcommands.add_parser('create-changelog', help='Create the replication changelog') repl_create_cl.set_defaults(func=create_cl) - + repl_delete_cl = repl_subcommands.add_parser('delete-changelog', help='Delete the replication changelog. This will invalidate any existing replication agreements') repl_delete_cl.set_defaults(func=delete_cl) @@ -1347,6 +1402,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') @@ -1364,7 +1421,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 " @@ -1379,11 +1439,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") @@ -1397,6 +1466,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") @@ -1410,12 +1481,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 @@ -1485,10 +1566,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") @@ -1496,8 +1582,12 @@ 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: :") @@ -1517,6 +1607,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") @@ -1525,15 +1617,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 76324c02d9..c778332951 100644 --- a/src/lib389/lib389/utils.py +++ b/src/lib389/lib389/utils.py @@ -1770,3 +1770,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.")