Extension for replication authentication methods (#1081)
diff --git a/app/addons/documents/tests/nightwatch/replicateDatabaseButton.js b/app/addons/documents/tests/nightwatch/replicateDatabaseButton.js
index 7d540ca..711a348 100644
--- a/app/addons/documents/tests/nightwatch/replicateDatabaseButton.js
+++ b/app/addons/documents/tests/nightwatch/replicateDatabaseButton.js
@@ -11,27 +11,27 @@
// the License.
-var helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
-var testDbName = 'test_database';
+const helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
+const testDbName = 'test_database';
module.exports = {
before: function (client, done) {
- var nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
+ const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
nano.db.create(testDbName, function () {
done();
});
},
after: function (client, done) {
- var nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
+ const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
nano.db.destroy(testDbName, function () {
done();
});
},
'Shows correct view on replicate database': function (client) {
- var waitTime = client.globals.maxWaitTime,
- baseUrl = client.globals.test_settings.launch_url;
- var srcDbSelector = '.replication__page .replication__section:nth-child(2) .replication__input-react-select .Select-value-label';
+ const waitTime = client.globals.maxWaitTime,
+ baseUrl = client.globals.test_settings.launch_url;
+ const srcDbSelector = '.replication__page .replication__section:nth-child(3) .replication__input-react-select .Select-value-label';
client
.loginToGUI()
.url(baseUrl + '/#/database/' + testDbName + '/_all_docs')
@@ -40,13 +40,13 @@
.clickWhenVisible('.faux-header__doc-header-dropdown-toggle')
.clickWhenVisible('.faux-header__doc-header-dropdown-itemwrapper .fonticon-replicate')
- //Wait for replication page to show up
+ //Wait for replication page to show up
.waitForElementVisible('.replication__page', waitTime, false)
- //Wait for source select to show
+ //Wait for source select to show
.waitForElementVisible(srcDbSelector, waitTime, false)
- //Get the text values
+ //Get the text values
.getText(srcDbSelector, function (data) {
this.verify.ok(data.value === testDbName,
'Check if database name is filled in source name');
diff --git a/app/addons/replication/__tests__/actions.test.js b/app/addons/replication/__tests__/actions.test.js
index 2e090e8..929a2c7 100644
--- a/app/addons/replication/__tests__/actions.test.js
+++ b/app/addons/replication/__tests__/actions.test.js
@@ -115,8 +115,12 @@
"replicationType": "REPLICATION_TYPE_ONE_TIME",
"replicationSource": "REPLICATION_SOURCE_LOCAL",
"localSource": "animaldb",
+ "sourceAuthType":"BASIC_AUTH",
+ "sourceAuth":{"username":"tester", "password":"testerpass"},
"replicationTarget": "REPLICATION_TARGET_EXISTING_LOCAL_DATABASE",
- "localTarget": "boom123"
+ "localTarget": "boom123",
+ "targetAuthType":"BASIC_AUTH",
+ "targetAuth":{"username":"tester", "password":"testerpass"}
};
it('builds up correct state', (done) => {
@@ -130,6 +134,60 @@
fetchMock.getOnce('/_replicator/7dcea9874a8fcb13c6630a1547001559', doc);
getReplicationStateFrom(doc._id)(dispatch);
});
+
+ it('builds up correct state with custom auth', (done) => {
+ const docWithCustomAuth = Object.assign(
+ {}, doc, {
+ "_id": "rep_custom_auth",
+ "continuous": true,
+ "source": {
+ "headers": {},
+ "url": "http://dev:8000/animaldb",
+ "auth": {
+ "creds": "source_user_creds"
+ }
+ },
+ "target": {
+ "headers": {},
+ "url": "http://dev:8000/boom123",
+ "auth": {
+ "creds": "target_user_creds"
+ }
+ }
+ });
+
+ const docStateWithCustomAuth = {
+ "replicationDocName": "rep_custom_auth",
+ "replicationType": "REPLICATION_TYPE_CONTINUOUS",
+ "replicationSource": "REPLICATION_SOURCE_LOCAL",
+ "localSource": "animaldb",
+ "sourceAuthType":"TEST_CUSTOM_AUTH",
+ "sourceAuth":{"creds":"source_user_creds"},
+ "replicationTarget": "REPLICATION_TARGET_EXISTING_LOCAL_DATABASE",
+ "localTarget": "boom123",
+ "targetAuthType":"TEST_CUSTOM_AUTH",
+ "targetAuth":{"creds":"target_user_creds"},
+ };
+ FauxtonAPI.registerExtension('Replication:Auth', {
+ typeValue: 'TEST_CUSTOM_AUTH',
+ typeLabel: 'Test Custom Auth',
+ getCredentials: (repSourceOrTarget) => {
+ if (repSourceOrTarget.auth && repSourceOrTarget.auth.creds) {
+ return { creds: repSourceOrTarget.auth.creds };
+ }
+ return undefined;
+ }
+ });
+ const dispatch = ({type, options}) => {
+ if (ActionTypes.REPLICATION_SET_STATE_FROM_DOC === type) {
+ assert.deepEqual(docStateWithCustomAuth, options);
+ setTimeout(done);
+ }
+ };
+
+ fetchMock.getOnce('/_replicator/rep_custom_auth', docWithCustomAuth);
+ getReplicationStateFrom(docWithCustomAuth._id)(dispatch);
+ });
});
describe('deleteDocs', () => {
diff --git a/app/addons/replication/__tests__/api.tests.js b/app/addons/replication/__tests__/api.tests.js
index e7f31b0..113c75b 100644
--- a/app/addons/replication/__tests__/api.tests.js
+++ b/app/addons/replication/__tests__/api.tests.js
@@ -9,6 +9,7 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+import FauxtonAPI from '../../../core/api';
import utils from '../../../../test/mocha/testUtils';
import {
getSource,
@@ -66,12 +67,14 @@
it('returns local source with auth info and encoded', () => {
const localSource = 'my/db';
-
const source = getSource({
replicationSource: Constants.REPLICATION_SOURCE.LOCAL,
localSource,
- username: 'the-user',
- password: 'password'
+ sourceAuth: {
+ username: 'the-user',
+ password: 'password'
+ },
+ sourceAuthType: Constants.REPLICATION_AUTH_METHOD.BASIC
}, {origin: 'http://dev:6767'});
assert.deepEqual(source.headers, {Authorization:"Basic dGhlLXVzZXI6cGFzc3dvcmQ="});
@@ -81,15 +84,62 @@
it('returns remote source url and auth header', () => {
const source = getSource({
replicationSource: Constants.REPLICATION_SOURCE.REMOTE,
- remoteSource: 'http://eddie:my-password@my-couchdb.com/my-db',
+ remoteSource: 'http://my-couchdb.com/my-db',
localSource: "local",
- username: 'the-user',
- password: 'password'
+ sourceAuth: {
+ username: 'the-user',
+ password: 'password'
+ },
+ sourceAuthType: Constants.REPLICATION_AUTH_METHOD.BASIC
}, {origin: 'http://dev:6767'});
- assert.deepEqual(source.headers, {Authorization:"Basic ZWRkaWU6bXktcGFzc3dvcmQ="});
+ assert.deepEqual(source.headers, {Authorization:"Basic dGhlLXVzZXI6cGFzc3dvcmQ="});
assert.deepEqual('http://my-couchdb.com/my-db', source.url);
});
+
+ it('returns source with no auth', () => {
+ const source = getSource({
+ replicationSource: Constants.REPLICATION_SOURCE.REMOTE,
+ remoteSource: 'http://my-couchdb.com/my-db',
+ localSource: "local",
+ sourceAuth: {
+ username: 'the-user',
+ password: 'password'
+ },
+ sourceAuthType: Constants.REPLICATION_AUTH_METHOD.NO_AUTH
+ }, {origin: 'http://dev:6767'});
+
+ assert.deepEqual(source.headers, {});
+
+ const source2 = getSource({
+ replicationSource: Constants.REPLICATION_SOURCE.REMOTE,
+ remoteSource: 'http://my-couchdb.com/my-db',
+ localSource: "local"
+ }, {origin: 'http://dev:6767'});
+
+ assert.deepEqual(source2.headers, {});
+ });
+
+ it('returns source with custom auth', () => {
+ FauxtonAPI.registerExtension('Replication:Auth', {
+ typeValue: 'TEST_CUSTOM_AUTH',
+ typeLabel: 'Test Custom Auth',
+ setCredentials: (repSourceOrTarget, auth) => {
+ repSourceOrTarget.auth = {
+ auth_creds: auth.creds
+ };
+ }
+ });
+ const source = getSource({
+ replicationSource: Constants.REPLICATION_SOURCE.REMOTE,
+ remoteSource: 'http://my-couchdb.com/my-db',
+ localSource: "local",
+ sourceAuth: { creds: 'sample_creds' },
+ sourceAuthType: 'TEST_CUSTOM_AUTH'
+ }, {origin: 'http://dev:6767'});
+
+ assert.deepEqual(source.auth, { auth_creds: 'sample_creds' });
+ });
});
describe('getTarget', () => {
@@ -104,12 +154,15 @@
});
it("encodes username and password for remote", () => {
- const remoteTarget = 'http://jimi:my-password@remote-couchdb.com/my/db';
+ const remoteTarget = 'http://remote-couchdb.com/my/db';
const target = getTarget({
replicationTarget: Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE,
remoteTarget: remoteTarget,
- username: 'fake',
- password: 'fake'
+ targetAuth: {
+ username: 'jimi',
+ password: 'my-password'
+ },
+ targetAuthType: Constants.REPLICATION_AUTH_METHOD.BASIC
});
assert.deepEqual(target.url, 'http://remote-couchdb.com/my%2Fdb');
@@ -120,8 +173,11 @@
const target = getTarget({
replicationTarget: Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE,
localTarget: 'my-existing/db',
- username: 'the-user',
- password: 'password'
+ targetAuth: {
+ username: 'the-user',
+ password: 'password'
+ },
+ targetAuthType: Constants.REPLICATION_AUTH_METHOD.BASIC
});
assert.deepEqual(target.headers, {Authorization:"Basic dGhlLXVzZXI6cGFzc3dvcmQ="});
@@ -133,8 +189,11 @@
replicationTarget: Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE,
replicationSource: Constants.REPLICATION_SOURCE.LOCAL,
localTarget: 'my-new/db',
- username: 'the-user',
- password: 'password'
+ targetAuth: {
+ username: 'the-user',
+ password: 'password'
+ },
+ targetAuthType: Constants.REPLICATION_AUTH_METHOD.BASIC
});
assert.deepEqual(target.headers, {Authorization:"Basic dGhlLXVzZXI6cGFzc3dvcmQ="});
@@ -146,8 +205,11 @@
replicationTarget: Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE,
replicationSource: Constants.REPLICATION_SOURCE.REMOTE,
localTarget: 'my-new/db',
- username: 'the-user',
- password: 'password'
+ targetAuth: {
+ username: 'the-user',
+ password: 'password'
+ },
+ targetAuthType: Constants.REPLICATION_AUTH_METHOD.BASIC
}, {origin: 'http://dev:5555'});
assert.deepEqual(target.headers, {Authorization:"Basic dGhlLXVzZXI6cGFzc3dvcmQ="});
@@ -173,6 +235,35 @@
assert.deepEqual("http://dev:8000/my-new%2Fdb", target.url);
assert.deepEqual({}, target.headers);
+
+ const targetNoAuth = getTarget({
+ replicationTarget: Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE,
+ replicationSource: Constants.REPLICATION_SOURCE.REMOTE,
+ localTarget: 'my-new/db',
+ targetAuthType: Constants.REPLICATION_AUTH_METHOD.NO_AUTH
+ }, location);
+ assert.deepEqual({}, targetNoAuth.headers);
+ });
+
+ it('returns target with custom auth', () => {
+ FauxtonAPI.registerExtension('Replication:Auth', {
+ typeValue: 'TEST_CUSTOM_AUTH',
+ typeLabel: 'Test Custom Auth',
+ setCredentials: (repSourceOrTarget, auth) => {
+ repSourceOrTarget.auth = {
+ auth_creds: auth.creds
+ };
+ }
+ });
+ const target = getTarget({
+ replicationTarget: Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE,
+ replicationSource: Constants.REPLICATION_SOURCE.LOCAL,
+ localTarget: 'my-new/db',
+ targetAuth: { creds: 'sample_creds' },
+ targetAuthType: 'TEST_CUSTOM_AUTH'
+ });
+
+ assert.deepEqual(target.auth, { auth_creds: 'sample_creds' });
});
});
@@ -331,6 +422,30 @@
});
+ describe('setCredentials', () => {
+
+ it('returns true for support', () => {
+ fetchMock.getOnce('/_scheduler/jobs', {});
+ return supportNewApi(true)
+ .then(resp => {
+ assert.ok(resp);
+ });
+ });
+
+ it('returns false for no support', () => {
+ fetchMock.getOnce('/_scheduler/jobs', {
+ status: 404,
+ body: {error: "missing"}
+ });
+
+ return supportNewApi(true)
+ .then(resp => {
+ assert.notOk(resp);
+ });
+ });
+
+ });
+
describe("fetchReplicationDocs", () => {
const _repDocs = {
"total_rows":2,
diff --git a/app/addons/replication/__tests__/newreplication.test.js b/app/addons/replication/__tests__/newreplication.test.js
index 7103a27..a5a9a2f 100644
--- a/app/addons/replication/__tests__/newreplication.test.js
+++ b/app/addons/replication/__tests__/newreplication.test.js
@@ -44,14 +44,34 @@
updateFormField={() => { return () => {}; }}
/>);
- assert.ok(newreplication.instance().validate());
+ assert.ok(newreplication.instance().checkSourceTargetDatabases());
});
it('returns true for remote source and target selected', () => {
const newreplication = shallow(<NewReplication
databases={[]}
replicationTarget={Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE}
- remoteTarget={"mydb"}
+ remoteTarget={"https://mydb.com/db2"}
+ remoteSource={"https://mydb.com/db1"}
+ localTarget={""}
+ localSource={""}
+ replicationSource={""}
+ replicationType={""}
+ replicationDocName={""}
+ conflictModalVisible={false}
+ clearReplicationForm={() => {}}
+ hideConflictModal={() => {}}
+ updateFormField={() => { return () => {}; }}
+ />);
+
+ assert.ok(newreplication.instance().checkSourceTargetDatabases());
+ });
+
+ it('returns false for invalid remote source', () => {
+ const newreplication = shallow(<NewReplication
+ databases={[]}
+ replicationTarget={Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE}
+ remoteTarget={"https://mydb.com/db"}
remoteSource={"anotherdb"}
localTarget={""}
localSource={""}
@@ -64,7 +84,27 @@
updateFormField={() => { return () => {}; }}
/>);
- assert.ok(newreplication.instance().validate());
+ assert.notOk(newreplication.instance().checkSourceTargetDatabases());
+ });
+
+ it('returns false for invalid remote target', () => {
+ const newreplication = shallow(<NewReplication
+ databases={[]}
+ replicationTarget={Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE}
+ remoteTarget={"anotherdb"}
+ remoteSource={"https://mydb.com/db"}
+ localTarget={""}
+ localSource={""}
+ replicationSource={""}
+ replicationType={""}
+ replicationDocName={""}
+ conflictModalVisible={false}
+ clearReplicationForm={() => {}}
+ hideConflictModal={() => {}}
+ updateFormField={() => { return () => {}; }}
+ />);
+
+ assert.notOk(newreplication.instance().checkSourceTargetDatabases());
});
it("warns if new local database exists", () => {
@@ -86,7 +126,7 @@
updateFormField={() => { return () => {}; }}
/>);
- newreplication.instance().validate();
+ newreplication.instance().checkSourceTargetDatabases();
assert.ok(spy.calledOnce);
const notification = spy.args[0][0];
@@ -112,7 +152,7 @@
updateFormField={() => { return () => {}; }}
/>);
- newreplication.instance().validate();
+ newreplication.instance().checkSourceTargetDatabases();
assert.ok(spy.calledOnce);
const notification = spy.args[0][0];
@@ -138,7 +178,7 @@
updateFormField={() => { return () => {}; }}
/>);
- newreplication.instance().validate();
+ newreplication.instance().checkSourceTargetDatabases();
assert.ok(spy.calledOnce);
const notification = spy.args[0][0];
@@ -151,8 +191,8 @@
const newreplication = shallow(<NewReplication
databases={[]}
replicationTarget={Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE}
- remoteTarget={"samedb"}
- remoteSource={"samedb"}
+ remoteTarget={"http://localhost/samedb"}
+ remoteSource={"http://localhost/samedb"}
localTarget={""}
localSource={""}
replicationSource={""}
@@ -164,7 +204,7 @@
updateFormField={() => { return () => {}; }}
/>);
- newreplication.instance().validate();
+ newreplication.instance().checkSourceTargetDatabases();
assert.ok(spy.calledOnce);
const notification = spy.args[0][0];
@@ -301,9 +341,9 @@
newreplication.instance().runReplicationChecks();
});
- it("Shows password modal", () => {
+ it("calls auth checks", () => {
let called = false;
- const showPasswordModal = () => {called = true;};
+ const checkAuth = () => {called = true;};
const checkReplicationDocID = () => {
const promise = FauxtonAPI.Deferred();
promise.resolve(false);
@@ -326,11 +366,51 @@
updateFormField={() => { return () => {}; }}
/>);
- newreplication.instance().showPasswordModal = showPasswordModal;
+ newreplication.instance().checkAuth = checkAuth;
newreplication.instance().runReplicationChecks();
assert.ok(called);
});
});
+ describe("checkAuth", () => {
+
+ afterEach(() => {
+ restore(FauxtonAPI.addNotification);
+ FauxtonAPI.session = undefined;
+ });
+
+ it("prompts user for local target auth method", () => {
+ const spy = sinon.spy(FauxtonAPI, 'addNotification');
+ FauxtonAPI.session = {
+ isAdminParty: () => false
+ };
+ const newreplication = shallow(<NewReplication
+ replicationDocName="my-doc-id"
+ checkReplicationDocID={() => {}}
+ showConflictModal={() => {}}
+ databases={[]}
+ replicationSource={Constants.REPLICATION_SOURCE.REMOTE}
+ replicationTarget={Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE}
+ targetAuthType={Constants.REPLICATION_AUTH_METHOD.NO_AUTH}
+ remoteTarget={""}
+ remoteSource={""}
+ localTarget={""}
+ localSource={""}
+ replicationType={""}
+ conflictModalVisible={false}
+ clearReplicationForm={() => {}}
+ hideConflictModal={() => {}}
+ updateFormField={() => { return () => {}; }}
+ />);
+
+ newreplication.instance().checkAuth();
+ sinon.assert.calledWith(spy, {
+ msg: 'Missing credentials for local target database.',
+ clear: true,
+ type: 'error'
+ });
+ });
+ });
+
});
diff --git a/app/addons/replication/actions.js b/app/addons/replication/actions.js
index ba8199c..08e0733 100644
--- a/app/addons/replication/actions.js
+++ b/app/addons/replication/actions.js
@@ -9,6 +9,7 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+import base64 from 'base-64';
import FauxtonAPI from '../../core/api';
import {get, post} from '../../core/ajax';
import ActionTypes from './actiontypes';
@@ -87,10 +88,8 @@
if (json.error && json.error === "not_found") {
return createReplicatorDB().then(() => {
return replicate(params);
- })
- .catch(handleError);
+ }).catch(handleError);
}
-
handleError(json);
});
};
@@ -283,6 +282,45 @@
});
};
+const getAuthTypeAndCredentials = (repSourceOrTarget) => {
+ const authTypeAndCreds = {
+ type: Constants.REPLICATION_AUTH_METHOD.NO_AUTH,
+ creds: {}
+ };
+ if (repSourceOrTarget.headers && repSourceOrTarget.headers.Authorization) {
+ // Removes 'Basic ' prefix
+ const encodedCreds = repSourceOrTarget.headers.Authorization.substring(6);
+ const decodedCreds = base64.decode(encodedCreds);
+ authTypeAndCreds.type = Constants.REPLICATION_AUTH_METHOD.BASIC;
+ authTypeAndCreds.creds = {
+ username: decodedCreds.split(':')[0],
+ password: decodedCreds.split(':')[1]
+ };
+ return authTypeAndCreds;
+ }
+
+ // Tries to get creds using one of the custom auth methods
+ // The extension should provide:
+ // - 'getCredentials(obj)' method that extracts the credentials from obj which is the 'target'/'source' field of the replication doc.
+ // - 'typeValue' field with an arbitrary ID representing the auth type the extension supports.
+ const authExtensions = FauxtonAPI.getExtensions('Replication:Auth');
+ let credentials = undefined;
+ let customAuthType = undefined;
+ if (authExtensions) {
+ authExtensions.map(ext => {
+ if (!credentials && ext.getCredentials) {
+ credentials = ext.getCredentials(repSourceOrTarget);
+ customAuthType = ext.typeValue;
+ }
+ });
+ }
+ if (credentials) {
+ authTypeAndCreds.type = customAuthType;
+ authTypeAndCreds.creds = credentials;
+ }
+ return authTypeAndCreds;
+};
+
export const getReplicationStateFrom = (id) => dispatch => {
dispatch({
type: ActionTypes.REPLICATION_FETCHING_FORM_STATE
@@ -306,6 +344,9 @@
stateDoc.replicationSource = Constants.REPLICATION_SOURCE.REMOTE;
stateDoc.remoteSource = decodeFullUrl(sourceUrl);
}
+ const sourceAuth = getAuthTypeAndCredentials(doc.source);
+ stateDoc.sourceAuthType = sourceAuth.type;
+ stateDoc.sourceAuth = sourceAuth.creds;
if (targetUrl.indexOf(window.location.hostname) > -1) {
const url = new URL(targetUrl);
@@ -315,6 +356,9 @@
stateDoc.replicationTarget = Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE;
stateDoc.remoteTarget = decodeFullUrl(targetUrl);
}
+ const targetAuth = getAuthTypeAndCredentials(doc.target);
+ stateDoc.targetAuthType = targetAuth.type;
+ stateDoc.targetAuth = targetAuth.creds;
dispatch({
type: ActionTypes.REPLICATION_SET_STATE_FROM_DOC,
@@ -358,15 +402,3 @@
});
});
};
-
-export const showPasswordModal = () => {
- return {
- type: ActionTypes.REPLICATION_SHOW_PASSWORD_MODAL
- };
-};
-
-export const hidePasswordModal = () => {
- return {
- type: ActionTypes.REPLICATION_HIDE_PASSWORD_MODAL
- };
-};
diff --git a/app/addons/replication/api.js b/app/addons/replication/api.js
index 2e24064..363228d 100644
--- a/app/addons/replication/api.js
+++ b/app/addons/replication/api.js
@@ -95,53 +95,63 @@
replicationSource,
localSource,
remoteSource,
- username,
- password
+ sourceAuthType,
+ sourceAuth
},
{origin} = window.location) => {
- let url;
- let headers;
+
+ const source = {};
if (replicationSource === Constants.REPLICATION_SOURCE.LOCAL) {
- url = `${origin}/${localSource}`;
- headers = getAuthHeaders(username, password);
+ source.url = encodeFullUrl(`${origin}/${localSource}`);
} else {
- const credentials = getCredentialsFromUrl(remoteSource);
- headers = getAuthHeaders(credentials.username, credentials.password);
- url = removeCredentialsFromUrl(remoteSource);
+ source.url = encodeFullUrl(removeCredentialsFromUrl(remoteSource));
}
- return {
- headers,
- url: encodeFullUrl(url)
- };
+ setCredentials(source, sourceAuthType, sourceAuth);
+ return source;
};
export const getTarget = ({
replicationTarget,
localTarget,
remoteTarget,
- username,
- password
+ targetAuthType,
+ targetAuth
},
-{origin} = window.location //this allows us to mock out window.location for our tests
-) => {
+//this allows us to mock out window.location for our tests
+{origin} = window.location) => {
- const encodedLocalTarget = encodeURIComponent(localTarget);
- let headers = getAuthHeaders(username, password);
- let target = `${origin}/${encodedLocalTarget}`;
-
+ const target = {};
if (replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE ||
replicationTarget === Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE) {
-
- const credentials = getCredentialsFromUrl(remoteTarget);
- target = encodeFullUrl(removeCredentialsFromUrl(remoteTarget));
- headers = getAuthHeaders(credentials.username, credentials.password);
+ target.url = encodeFullUrl(removeCredentialsFromUrl(remoteTarget));
+ } else {
+ const encodedLocalTarget = encodeURIComponent(localTarget);
+ target.url = `${origin}/${encodedLocalTarget}`;
}
- return {
- headers: headers,
- url: target
- };
+ setCredentials(target, targetAuthType, targetAuth);
+ return target;
+};
+
+const setCredentials = (target, authType, auth) => {
+ if (!authType || authType === Constants.REPLICATION_AUTH_METHOD.NO_AUTH) {
+ target.headers = {};
+ } else if (authType === Constants.REPLICATION_AUTH_METHOD.BASIC) {
+ target.headers = getAuthHeaders(auth.username, auth.password);
+ } else {
+ // Tries to set creds using one of the custom auth methods
+ // The extension should provide:
+ // - 'setCredentials(target, auth)' method which sets the 'auth' credentials into 'target' which is the 'target'/'source' field of the replication doc.
+ const authExtensions = FauxtonAPI.getExtensions('Replication:Auth');
+ if (authExtensions) {
+ authExtensions.filter(ext => ext.typeValue === authType).map(ext => {
+ if (ext.setCredentials) {
+ ext.setCredentials(target, auth);
+ }
+ });
+ }
+ }
};
export const createTarget = (replicationTarget) => {
@@ -180,12 +190,15 @@
replicationSource,
replicationType,
replicationDocName,
- password,
localTarget,
localSource,
remoteTarget,
remoteSource,
- _rev
+ _rev,
+ sourceAuthType,
+ sourceAuth,
+ targetAuthType,
+ targetAuth
}) => {
const username = getUsername();
return addDocIdAndRev(replicationDocName, _rev, {
@@ -197,16 +210,16 @@
replicationSource,
localSource,
remoteSource,
- username,
- password
+ sourceAuthType,
+ sourceAuth
}),
target: getTarget({
replicationTarget,
replicationSource,
remoteTarget,
localTarget,
- username,
- password
+ targetAuthType,
+ targetAuth
}),
create_target: createTarget(replicationTarget),
continuous: continuous(replicationType),
diff --git a/app/addons/replication/assets/less/replication.less b/app/addons/replication/assets/less/replication.less
index c1a56de..6a8a1aa 100644
--- a/app/addons/replication/assets/less/replication.less
+++ b/app/addons/replication/assets/less/replication.less
@@ -13,6 +13,8 @@
@import "../../../../../assets/less/variables.less";
@import "../../../../../assets/less/mixins.less";
+@replication_input_field_width: 400px;
+
div.replication__page {
padding-top: 25px !important;
display: flex;
@@ -24,6 +26,10 @@
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
+ & input {
+ width: @replication_input_field_width;
+ font-size: 14px;
+ }
}
.replication__seperator {
@@ -44,27 +50,28 @@
width: 540px;
select {
font-size: 14px;
- width: 400px;
+ width: @replication_input_field_width;
margin-bottom: 10px;
background-color: white;
border: 1px solid #cccccc;
}
.styled-select {
- width: 400px;
+ width: @replication_input_field_width;
}
}
.replication__input-react-select {
font-size: 14px;
+ padding-bottom: 10px;
.Select .Select-menu-outer {
- width: 400px;
+ width: @replication_input_field_width;
}
.Select div.Select-control {
padding: 6px;
border: 1px solid #cccccc;
- width: 400px;
+ width: @replication_input_field_width;
.Select-value, .Select-placeholder {
padding: 6px 15px 6px 10px;
@@ -84,7 +91,7 @@
.replication__remote-connection-url[type="text"] {
font-size: 14px;
- width: 400px;
+ width: @replication_input_field_width;
color: #333;
}
@@ -95,14 +102,14 @@
}
.replication__new-input[type="text"] {
- width: 400px;
+ width: @replication_input_field_width;
font-size: 14px;
color: #333;
}
.replication__doc-name {
position: relative;
- width: 400px;
+ width: @replication_input_field_width;
}
@@ -125,7 +132,7 @@
.replication__doc-name-input[type="text"] {
padding-right: 32px;
font-size: 14px;
- width: 400px;
+ width: @replication_input_field_width;
color: #333;
}
@@ -342,4 +349,4 @@
.replication__activity-caveat {
padding-left: 80px;
-}
+}
\ No newline at end of file
diff --git a/app/addons/replication/components/auth-options.js b/app/addons/replication/components/auth-options.js
new file mode 100644
index 0000000..102a73e
--- /dev/null
+++ b/app/addons/replication/components/auth-options.js
@@ -0,0 +1,176 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import PropTypes from 'prop-types';
+import FauxtonAPI from '../../../core/api';
+import app from '../../../app';
+import React from 'react';
+import Constants from '../constants';
+import Components from '../../components/react-components';
+
+const { StyledSelect } = Components;
+
+export class ReplicationAuth extends React.Component {
+
+ constructor (props) {
+ super(props);
+ this.onChangeType = this.onChangeType.bind(this);
+ this.onChangeValue = this.onChangeValue.bind(this);
+
+ // init auth extensions
+ // The extension should provide:
+ // - 'inputComponent' a React component that will be displayed when the user selects the auth method.
+ // - 'typeValue' field with an arbitrary ID representing the auth type the extension supports.
+ // - 'typeLabel' field containing the display label for the authentication method.
+ this.customAuths = FauxtonAPI.getExtensions('Replication:Auth');
+ if (!this.customAuths) {
+ this.customAuths = [];
+ }
+ this.customAuthTypes = this.customAuths.map(auth => auth.typeValue);
+ }
+
+ getAuthOptions = () => {
+ const userPasswordLabel = app.i18n.en_US['replication-user-password-auth-label'];
+ const authOptions = [
+ { value: Constants.REPLICATION_AUTH_METHOD.NO_AUTH, label: 'None' },
+ { value: Constants.REPLICATION_AUTH_METHOD.BASIC, label: userPasswordLabel }
+ ];
+ this.customAuths.map(auth => {
+ authOptions.push({ value: auth.typeValue, label: auth.typeLabel });
+ });
+
+ return authOptions.map(option => <option value={option.value} key={option.value}>{option.label}</option>);
+ }
+
+ onChangeType(newType) {
+ this.props.onChangeAuthType(newType);
+ }
+
+ onChangeValue(newValue) {
+ this.props.onChangeAuth(newValue);
+ }
+
+ getAuthInputFields(authValue, authType) {
+ const {authId} = this.props;
+ if (authType == Constants.REPLICATION_AUTH_METHOD.BASIC) {
+ return <UserPasswordAuthInput onChange={this.onChangeValue} auth={authValue} authId={authId}/>;
+ }
+ const matchedAuths = this.customAuths.filter(el => el.typeValue === authType);
+ if (matchedAuths && matchedAuths.length > 0) {
+ const InputComp = matchedAuths[0].inputComponent;
+ return <InputComp onChange={this.onChangeValue} auth={authValue} />;
+ }
+
+ return null;
+ }
+
+ render () {
+ const {credentials, authType, authId} = this.props;
+ return (<React.Fragment>
+ <div className="replication__section">
+ <div className="replication__input-label">
+ Authentication:
+ </div>
+ <div className="replication__input-select">
+ <StyledSelect
+ selectContent={this.getAuthOptions()}
+ selectChange={(e) => this.onChangeType(e.target.value)}
+ selectId={'select-' + authId}
+ selectValue={authType} />
+ </div>
+ </div>
+ {this.getAuthInputFields(credentials, authType)}
+ </React.Fragment>);
+ }
+}
+
+ReplicationAuth.propTypes = {
+ authId: PropTypes.string.isRequired,
+ authType: PropTypes.string.isRequired,
+ credentials: PropTypes.object,
+ onChangeAuth: PropTypes.func.isRequired,
+ onChangeAuthType: PropTypes.func.isRequired
+};
+
+ReplicationAuth.defaultProps = {
+ authType: Constants.REPLICATION_AUTH_METHOD.NO_AUTH,
+ onChangeAuthType: () => {},
+ onChangeAuth: () => {}
+};
+
+export class UserPasswordAuthInput extends React.Component {
+
+ constructor (props) {
+ super(props);
+ this.updatePassword = this.updatePassword.bind(this);
+ this.updateUsername = this.updateUsername.bind(this);
+ this.state = {
+ username: props.auth && props.auth.username ? props.auth.username : '',
+ password: props.auth && props.auth.password ? props.auth.password : ''
+ };
+ }
+
+ updatePassword(newValue) {
+ this.setState({password: newValue});
+ this.props.onChange({
+ username: this.state.username,
+ password: newValue
+ });
+ }
+
+ updateUsername(newValue) {
+ this.setState({username: newValue});
+ this.props.onChange({
+ username: newValue,
+ password: this.state.password
+ });
+ }
+
+ render () {
+ const usernamePlaceholder = app.i18n.en_US['replication-username-input-placeholder'];
+ const passwordPlaceholder = app.i18n.en_US['replication-password-input-placeholder'];
+ const { authId } = this.props;
+ return (
+ <React.Fragment>
+ <div className="replication__section">
+ <div className="replication__input-label"></div>
+ <div>
+ <input
+ id={authId + '-username'}
+ type="text"
+ placeholder={usernamePlaceholder}
+ value={this.state.username}
+ onChange={(e) => this.updateUsername(e.target.value)}
+ readOnly={this.props.usernameReadOnly}
+ />
+ </div>
+ </div>
+ <div className="replication__section">
+ <div className="replication__input-label"></div>
+ <div>
+ <input
+ id={authId + '-password'}
+ type="password"
+ placeholder={passwordPlaceholder}
+ value={this.state.password}
+ onChange={(e) => this.updatePassword(e.target.value)}
+ />
+ </div>
+ </div>
+ </React.Fragment>
+ );
+ }
+}
+
+UserPasswordAuthInput.propTypes = {
+ auth: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired
+};
diff --git a/app/addons/replication/components/newreplication.js b/app/addons/replication/components/newreplication.js
index f1a82a0..87b37e2 100644
--- a/app/addons/replication/components/newreplication.js
+++ b/app/addons/replication/components/newreplication.js
@@ -11,49 +11,94 @@
// the License.
import React from 'react';
-import app from '../../../app';
import FauxtonAPI from '../../../core/api';
import {ReplicationSource} from './source';
import {ReplicationTarget} from './target';
import {ReplicationOptions} from './options';
import {ReplicationSubmit} from './submit';
-import AuthComponents from '../../auth/components';
+import {ReplicationAuth} from './auth-options';
+import AuthAPI from '../../auth/api';
import Constants from '../constants';
import {ConflictModal} from './modals';
import {isEmpty} from 'lodash';
-const {PasswordModal} = AuthComponents;
-
export default class NewReplicationController extends React.Component {
constructor (props) {
super(props);
this.submit = this.submit.bind(this);
- this.clear = this.clear.bind(this);
- this.showPasswordModal = this.showPasswordModal.bind(this);
+ this.checkAuth = this.checkAuth.bind(this);
this.runReplicationChecks = this.runReplicationChecks.bind(this);
}
- clear (e) {
- e.preventDefault();
- this.props.clearReplicationForm();
- }
-
- showPasswordModal () {
+ checkAuth () {
this.props.hideConflictModal();
- const { replicationSource, replicationTarget } = this.props;
+ const { replicationSource, replicationTarget,
+ sourceAuthType, targetAuthType, sourceAuth, targetAuth } = this.props;
- const hasLocalSourceOrTarget = (replicationSource === Constants.REPLICATION_SOURCE.LOCAL ||
- replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE ||
- replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE);
+ const isLocalSource = replicationSource === Constants.REPLICATION_SOURCE.LOCAL;
+ const isLocalTarget = replicationTarget === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE ||
+ replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE;
- // if the user is authenticated, or if NEITHER the source nor target are local, just submit. The password
- // modal isn't necessary or if couchdb is in admin party mode
- if (!hasLocalSourceOrTarget || this.props.authenticated || FauxtonAPI.session.isAdminParty()) {
- this.submit(this.props.username, this.props.password);
- return;
+ // Ask user to select an auth method for local source/target when one is not selected
+ // and not on admin party
+ if (!FauxtonAPI.session.isAdminParty()) {
+ if (isLocalSource && sourceAuthType === Constants.REPLICATION_AUTH_METHOD.NO_AUTH) {
+ FauxtonAPI.addNotification({
+ msg: 'Missing credentials for local source database.',
+ type: 'error',
+ clear: true
+ });
+ return;
+ }
+ if (isLocalTarget && targetAuthType === Constants.REPLICATION_AUTH_METHOD.NO_AUTH) {
+ FauxtonAPI.addNotification({
+ msg: 'Missing credentials for local target database.',
+ type: 'error',
+ clear: true
+ });
+ return;
+ }
}
- this.props.showPasswordModal();
+ this.checkLocalAccountCredentials(sourceAuthType, sourceAuth, 'source', isLocalSource).then(() => {
+ this.checkLocalAccountCredentials(targetAuthType, targetAuth, 'target', isLocalTarget).then(() => {
+ this.submit();
+ }, () => {});
+ }, () => {});
+ }
+
+ checkLocalAccountCredentials(authType, auth, label, isLocal) {
+ // Skip check if it's a remote tb or not using BASIC auth
+ if (authType !== Constants.REPLICATION_AUTH_METHOD.BASIC || !isLocal) {
+ return FauxtonAPI.Promise.resolve(true);
+ }
+
+ if (!auth.username || !auth.password) {
+ const err = `Missing ${label} credentials.`;
+ FauxtonAPI.addNotification({
+ msg: err,
+ type: 'error',
+ clear: true
+ });
+ return FauxtonAPI.Promise.reject(new Error(err));
+ }
+
+ return AuthAPI.login({
+ name: auth.username,
+ password: auth.password
+ }).then((resp) => {
+ if (resp.error) {
+ throw (resp);
+ }
+ return true;
+ }).catch(err => {
+ FauxtonAPI.addNotification({
+ msg: `Your username or password for ${label} database is incorrect.`,
+ type: 'error',
+ clear: true
+ });
+ throw err;
+ });
}
checkReplicationDocID () {
@@ -64,13 +109,13 @@
return;
}
- this.showPasswordModal();
+ this.checkAuth();
});
}
runReplicationChecks () {
const {replicationDocName} = this.props;
- if (!this.validate()) {
+ if (!this.checkSourceTargetDatabases()) {
return;
}
if (replicationDocName) {
@@ -78,10 +123,10 @@
return;
}
- this.showPasswordModal();
+ this.checkAuth();
}
- validate () {
+ checkSourceTargetDatabases () {
const {
remoteTarget,
remoteSource,
@@ -120,6 +165,48 @@
}
}
+ //check if remote source/target URL is valid
+ if (!isEmpty(remoteSource)) {
+ let errorMessage = '';
+ try {
+ const url = new URL(remoteSource);
+ if (url.pathname.slice(1) === '') {
+ errorMessage = 'Invalid source database URL. Database name is missing.';
+ }
+ } catch (err) {
+ errorMessage = 'Invalid source database URL.';
+ }
+ if (errorMessage) {
+ FauxtonAPI.addNotification({
+ msg: errorMessage,
+ type: 'error',
+ escape: false,
+ clear: true
+ });
+ return false;
+ }
+ }
+ if (!isEmpty(remoteTarget)) {
+ let errorMessage = '';
+ try {
+ const url = new URL(remoteTarget);
+ if (url.pathname.slice(1) === '') {
+ errorMessage = 'Invalid target database URL. Database name is missing.';
+ }
+ } catch (err) {
+ errorMessage = 'Invalid target database URL.';
+ }
+ if (errorMessage) {
+ FauxtonAPI.addNotification({
+ msg: errorMessage,
+ type: 'error',
+ escape: false,
+ clear: true
+ });
+ return false;
+ }
+ }
+
//check that source and target are not the same. They can trigger a false positive if they are ""
if ((remoteTarget === remoteSource && !isEmpty(remoteTarget))
|| (localSource === localTarget && !isEmpty(localSource))) {
@@ -136,7 +223,7 @@
return true;
}
- submit (username, password) {
+ submit () {
const {
replicationTarget,
replicationSource,
@@ -145,7 +232,11 @@
remoteTarget,
remoteSource,
localTarget,
- localSource
+ localSource,
+ sourceAuthType,
+ sourceAuth,
+ targetAuthType,
+ targetAuth
} = this.props;
let _rev;
@@ -156,19 +247,20 @@
}
}
- this.props.hidePasswordModal();
this.props.replicate({
replicationTarget,
replicationSource,
replicationType,
replicationDocName,
- username,
- password,
localTarget,
localSource,
remoteTarget,
remoteSource,
- _rev
+ _rev,
+ sourceAuthType,
+ sourceAuth,
+ targetAuthType,
+ targetAuth
});
}
@@ -215,7 +307,6 @@
replicationTarget,
replicationType,
replicationDocName,
- passwordModalVisible,
conflictModalVisible,
databases,
localSource,
@@ -223,11 +314,15 @@
remoteTarget,
localTarget,
updateFormField,
- clearReplicationForm
+ clearReplicationForm,
+ sourceAuthType,
+ sourceAuth,
+ targetAuthType,
+ targetAuth
} = this.props;
return (
- <div>
+ <div style={ {paddingBottom: 20} }>
<ReplicationSource
replicationSource={replicationSource}
localSource={localSource}
@@ -237,6 +332,13 @@
onRemoteSourceChange={updateFormField('remoteSource')}
onLocalSourceChange={updateFormField('localSource')}
/>
+ <ReplicationAuth
+ credentials={sourceAuth}
+ authType={sourceAuthType}
+ onChangeAuthType={updateFormField('sourceAuthType')}
+ onChangeAuth={updateFormField('sourceAuth')}
+ authId={'replication-source-auth'}
+ />
<hr className="replication__seperator" size="1"/>
<ReplicationTarget
replicationTarget={replicationTarget}
@@ -247,6 +349,13 @@
onRemoteTargetChange={updateFormField('remoteTarget')}
onLocalTargetChange={updateFormField('localTarget')}
/>
+ <ReplicationAuth
+ credentials={targetAuth}
+ authType={targetAuthType}
+ onChangeAuthType={updateFormField('targetAuthType')}
+ onChangeAuth={updateFormField('targetAuth')}
+ authId={'replication-target-auth'}
+ />
<hr className="replication__seperator" size="1"/>
<ReplicationOptions
replicationType={replicationType}
@@ -259,16 +368,9 @@
onClick={this.runReplicationChecks}
onClear={clearReplicationForm}
/>
- <PasswordModal
- visible={passwordModalVisible}
- modalMessage={<p>{app.i18n.en_US['replication-password-modal-text']}</p>}
- submitBtnLabel="Start Replication"
- headerTitle={app.i18n.en_US['replication-password-modal-header']}
- onClose={this.props.hidePasswordModal}
- onSuccess={this.submit} />
<ConflictModal
visible={conflictModalVisible}
- onClick={this.showPasswordModal}
+ onClick={this.checkAuth}
onClose={this.props.hideConflictModal}
docId={replicationDocName}
/>
diff --git a/app/addons/replication/components/options.js b/app/addons/replication/components/options.js
index f00ac2e..686413a 100644
--- a/app/addons/replication/components/options.js
+++ b/app/addons/replication/components/options.js
@@ -28,7 +28,7 @@
return (
<div className="replication__section">
<div className="replication__input-label">
- Replication Type:
+ Replication type:
</div>
<div className="replication__input-select">
<StyledSelect
@@ -49,7 +49,7 @@
const ReplicationDoc = ({value, onChange}) =>
<div className="replication__section">
<div className="replication__input-label">
- Replication Document:
+ Replication document:
</div>
<div className="replication__doc-name">
<span className="fonticon fonticon-cancel replication__doc-name-icon" title="Clear field"
@@ -76,6 +76,7 @@
return (
<div>
+ <h3>Options</h3>
<ReplicationType
onChange={onTypeChange}
value={replicationType}
diff --git a/app/addons/replication/components/source.js b/app/addons/replication/components/source.js
index 5b5e8c2..e4b6c33 100644
--- a/app/addons/replication/components/source.js
+++ b/app/addons/replication/components/source.js
@@ -15,7 +15,6 @@
import Constants from '../constants';
import Components from '../../components/react-components';
import ReactSelect from 'react-select';
-import RemoteExample from './remoteexample';
const { StyledSelect } = Components;
@@ -30,7 +29,6 @@
value={value}
onChange={(e) => onChange(e.target.value)}
/>
- <RemoteExample />
</div>
</div>;
@@ -44,7 +42,7 @@
return (
<div className="replication__section">
<div className="replication__input-label">
- Source Name:
+ Name:
</div>
<div className="replication__input-react-select">
<ReactSelect
@@ -88,7 +86,7 @@
const replicationSourceSelectOptions = () => {
return [
- { value: '', label: 'Select source' },
+ { value: '', label: 'Select source type' },
{ value: Constants.REPLICATION_SOURCE.LOCAL, label: 'Local database' },
{ value: Constants.REPLICATION_SOURCE.REMOTE, label: 'Remote database' }
].map((option) => {
@@ -103,7 +101,7 @@
return (
<div className="replication__section">
<div className="replication__input-label">
- Replication Source:
+ Type:
</div>
<div className="replication__input-select">
<StyledSelect
@@ -151,6 +149,7 @@
const {replicationSource, onSourceSelect} = this.props;
return (
<div>
+ <h3>Source</h3>
<ReplicationSourceSelect
onChange={onSourceSelect}
value={replicationSource}
diff --git a/app/addons/replication/components/target.js b/app/addons/replication/components/target.js
index 255bc24..4cfe1c4 100644
--- a/app/addons/replication/components/target.js
+++ b/app/addons/replication/components/target.js
@@ -15,13 +15,12 @@
import Constants from '../constants';
import Components from '../../components/react-components';
import ReactSelect from 'react-select';
-import RemoteExample from './remoteexample';
const { StyledSelect } = Components;
const replicationTargetSourceOptions = () => {
return [
- { value: '', label: 'Select target' },
+ { value: '', label: 'Select target type' },
{ value: Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE, label: 'Existing local database' },
{ value: Constants.REPLICATION_TARGET.EXISTING_REMOTE_DATABASE, label: 'Existing remote database' },
{ value: Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE, label: 'New local database' },
@@ -37,7 +36,7 @@
return (
<div className="replication__section">
<div className="replication__input-label">
- Replication Target:
+ Type:
</div>
<div id="replication-target" className="replication__input-select">
<StyledSelect
@@ -55,7 +54,7 @@
onChange: PropTypes.func.isRequired
};
-const RemoteTargetReplicationRow = ({onChange, value, newRemote}) => {
+const RemoteTargetReplicationRow = ({onChange, value}) => {
return (
<div>
<input
@@ -65,7 +64,6 @@
value={value}
onChange={(e) => onChange(e.target.value)}
/>
- <RemoteExample newRemote={newRemote} />
</div>
);
};
@@ -124,7 +122,7 @@
let input;
if (replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) {
- targetLabel = 'New Database:';
+ targetLabel = 'New database:';
input = <NewLocalTargetReplicationRow
value={localTarget}
onChange={onLocalTargetChange}
@@ -143,11 +141,11 @@
/>;
}
- let targetLabel = 'Target Name:';
+ let targetLabel = 'Name:';
if (replicationTarget === Constants.REPLICATION_TARGET.NEW_REMOTE_DATABASE ||
replicationTarget === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE) {
- targetLabel = 'New Database:';
+ targetLabel = 'New database:';
}
return (
@@ -183,6 +181,7 @@
} = this.props;
return (
<div>
+ <h3>Target</h3>
<ReplicationTargetSelect
value={replicationTarget}
onChange={onTargetChange}
diff --git a/app/addons/replication/constants.js b/app/addons/replication/constants.js
index b9e699f..46567be 100644
--- a/app/addons/replication/constants.js
+++ b/app/addons/replication/constants.js
@@ -26,5 +26,10 @@
REPLICATION_TYPE: {
ONE_TIME: 'REPLICATION_TYPE_ONE_TIME',
CONTINUOUS: 'REPLICATION_TYPE_CONTINUOUS'
+ },
+
+ REPLICATION_AUTH_METHOD: {
+ NO_AUTH: 'NO_AUTH',
+ BASIC: 'BASIC_AUTH'
}
};
diff --git a/app/addons/replication/container.js b/app/addons/replication/container.js
index 7320ae3..b146843 100644
--- a/app/addons/replication/container.js
+++ b/app/addons/replication/container.js
@@ -10,8 +10,6 @@
getReplicateActivity,
getReplicationActivity,
getDatabasesList,
- showPasswordModal,
- hidePasswordModal,
showConflictModal,
hideConflictModal,
replicate,
@@ -30,7 +28,6 @@
isLoading,
isActivityLoading,
getDatabases,
- isAuthenticated,
getReplicationSource,
getLocalSource,
isLocalSourceKnown,
@@ -39,7 +36,6 @@
getLocalTarget,
isLocalTargetKnown,
getRemoteTarget,
- isPasswordModalVisible,
isConflictModalVisible,
getReplicationType,
getReplicationDocName,
@@ -68,22 +64,24 @@
loading: isLoading(replication),
activityLoading: isActivityLoading(replication),
databases: getDatabases(replication),
- authenticated: isAuthenticated(replication),
// source fields
replicationSource: getReplicationSource(replication),
localSource: getLocalSource(replication),
localSourceKnown: isLocalSourceKnown(replication),
remoteSource: getRemoteSource(replication),
+ sourceAuthType: replication.sourceAuthType,
+ sourceAuth: replication.sourceAuth,
// target fields
replicationTarget: getReplicationTarget(replication),
localTarget: getLocalTarget(replication),
localTargetKnown: isLocalTargetKnown(replication),
remoteTarget: getRemoteTarget(replication),
+ targetAuthType: replication.targetAuthType,
+ targetAuth: replication.targetAuth,
// other
- passwordModalVisible: isPasswordModalVisible(replication),
isConflictModalVisible: isConflictModalVisible(replication),
replicationType: getReplicationType(replication),
replicationDocName: getReplicationDocName(replication),
@@ -117,8 +115,6 @@
getReplicateActivity: () => dispatch(getReplicateActivity()),
getReplicationStateFrom: (id) => dispatch(getReplicationStateFrom(id)),
getDatabasesList: () => dispatch(getDatabasesList()),
- showPasswordModal: () => dispatch(showPasswordModal()),
- hidePasswordModal: () => dispatch(hidePasswordModal()),
replicate: (params) => dispatch(replicate(params)),
showConflictModal: () => dispatch(showConflictModal()),
hideConflictModal: () => dispatch(hideConflictModal()),
diff --git a/app/addons/replication/controller.js b/app/addons/replication/controller.js
index 117714a..a65febe 100644
--- a/app/addons/replication/controller.js
+++ b/app/addons/replication/controller.js
@@ -62,14 +62,15 @@
showSection () {
const {
replicationSource, replicationTarget, replicationType, replicationDocName,
- passwordModalVisible, databases, localSource, remoteSource, remoteTarget,
+ databases, localSource, remoteSource, remoteTarget,
localTarget, statusDocs, statusFilter, loading, allDocsSelected,
someDocsSelected, showConflictModal, localSourceKnown, localTargetKnown, updateFormField,
- username, password, authenticated, activityLoading, submittedNoChange, activitySort, tabSection,
+ authenticated, activityLoading, submittedNoChange, activitySort, tabSection,
replicateInfo, replicateLoading, replicateFilter, allReplicateSelected, someReplicateSelected,
- showPasswordModal, hidePasswordModal, hideConflictModal, isConflictModalVisible, filterDocs,
+ hideConflictModal, isConflictModalVisible, filterDocs,
filterReplicate, replicate, clearReplicationForm, selectAllDocs, changeActivitySort, selectDoc,
- deleteDocs, deleteReplicates, selectAllReplicates, selectReplicate
+ deleteDocs, deleteReplicates, selectAllReplicates, selectReplicate,
+ sourceAuthType, sourceAuth, targetAuthType, targetAuth
} = this.props;
if (tabSection === 'new replication') {
@@ -83,27 +84,26 @@
localSourceKnown={localSourceKnown}
clearReplicationForm={clearReplicationForm}
replicate={replicate}
- showPasswordModal={showPasswordModal}
replicationSource={replicationSource}
replicationTarget={replicationTarget}
replicationType={replicationType}
replicationDocName={replicationDocName}
- passwordModalVisible={passwordModalVisible}
databases={databases}
localSource={localSource}
remoteSource={remoteSource}
remoteTarget={remoteTarget}
localTarget={localTarget}
+ sourceAuthType={sourceAuthType}
+ sourceAuth={sourceAuth}
+ targetAuthType={targetAuthType}
+ targetAuth={targetAuth}
updateFormField={updateFormField}
conflictModalVisible={isConflictModalVisible}
hideConflictModal={hideConflictModal}
showConflictModal={showConflictModal}
checkReplicationDocID={checkReplicationDocID}
authenticated={authenticated}
- username={username}
- password={password}
submittedNoChange={submittedNoChange}
- hidePasswordModal={hidePasswordModal}
/>;
}
diff --git a/app/addons/replication/reducers.js b/app/addons/replication/reducers.js
index 545e98d..15b6a53 100644
--- a/app/addons/replication/reducers.js
+++ b/app/addons/replication/reducers.js
@@ -9,6 +9,7 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+import FauxtonAPI from '../../core/api';
import ActionTypes from './actiontypes';
import Constants from './constants';
import app from "../../app";
@@ -41,26 +42,32 @@
replicationDocName: 'replicationDocName',
replicationSource: 'replicationSource',
replicationTarget: 'replicationTarget',
- localSource: 'localSource'
+ localSource: 'localSource',
+ sourceAuthType: 'sourceAuthType',
+ sourceAuth: 'sourceAuth',
+ targetAuthType: 'targetAuthType',
+ targetAuth: 'targetAuth'
};
const initialState = {
loading: false,
databases: [],
- authenticated: false,
// source fields
replicationSource: '',
localSource: '',
remoteSource: '',
+ sourceAuthType: Constants.REPLICATION_AUTH_METHOD.NO_AUTH,
+ sourceAuth: {},
// target fields
replicationTarget: '',
localTarget: '',
remoteTarget: '',
+ targetAuthType: Constants.REPLICATION_AUTH_METHOD.NO_AUTH,
+ targetAuth: {},
// other
- isPasswordModalVisible: false,
isConflictModalVisible: false,
replicationType: Constants.REPLICATION_TYPE.ONE_TIME,
replicationDocName: '',
@@ -87,7 +94,15 @@
const newState = {
...state
};
- Object.values(validFieldMap).forEach(field => newState[field] = '');
+ Object.values(validFieldMap).forEach(field => {
+ if (field === 'sourceAuth' || field === 'targetAuth') {
+ newState[field] = {};
+ } else {
+ newState[field] = '';
+ }
+ });
+ newState.sourceAuthType = Constants.REPLICATION_AUTH_METHOD.NO_AUTH;
+ newState.targetAuthType = Constants.REPLICATION_AUTH_METHOD.NO_AUTH;
return newState;
};
@@ -96,9 +111,32 @@
...state,
submittedNoChange: false,
};
-
updateState[validFieldMap[fieldName]] = value;
+ // Set default username when state is set to local target/source AND auth is user/pwd
+ if (fieldName === validFieldMap.sourceAuthType || fieldName === validFieldMap.replicationSource) {
+ const isUserPwdAuth = updateState[validFieldMap.sourceAuthType] === Constants.REPLICATION_AUTH_METHOD.BASIC;
+ const isLocalDB = updateState[validFieldMap.replicationSource] === Constants.REPLICATION_SOURCE.LOCAL;
+ const usernameNotSet = !updateState[validFieldMap.sourceAuth] || !updateState[validFieldMap.sourceAuth].username;
+ if (isUserPwdAuth && isLocalDB && usernameNotSet) {
+ updateState[validFieldMap.sourceAuth] = {
+ username: FauxtonAPI.session.user().name,
+ password: ''
+ };
+ }
+ } else if (fieldName === validFieldMap.targetAuthType || fieldName === validFieldMap.replicationTarget) {
+ const isUserPwdAuth = updateState[validFieldMap.targetAuthType] === Constants.REPLICATION_AUTH_METHOD.BASIC;
+ const isLocalDB = updateState[validFieldMap.replicationTarget] === Constants.REPLICATION_TARGET.EXISTING_LOCAL_DATABASE ||
+ updateState[validFieldMap.replicationTarget] === Constants.REPLICATION_TARGET.NEW_LOCAL_DATABASE;
+ const usernameNotSet = !updateState[validFieldMap.targetAuth] || !updateState[validFieldMap.targetAuth].username;
+ if (isUserPwdAuth && isLocalDB && usernameNotSet) {
+ updateState[validFieldMap.targetAuth] = {
+ username: FauxtonAPI.session.user().name,
+ password: ''
+ };
+ }
+ }
+
return updateState;
};
@@ -306,18 +344,6 @@
allReplicateSelected: false
};
- case ActionTypes.REPLICATION_SHOW_PASSWORD_MODAL:
- return {
- ...state,
- isPasswordModalVisible: true
- };
-
- case ActionTypes.REPLICATION_HIDE_PASSWORD_MODAL:
- return {
- ...state,
- isPasswordModalVisible: false
- };
-
default:
return state;
}
@@ -327,7 +353,6 @@
export const isLoading = (state) => state.isLoading;
export const isActivityLoading = (state) => state.activityLoading;
export const getDatabases = (state) => state.databases;
-export const isAuthenticated = (state) => state.authenticated;
export const getReplicationSource = (state) => state.replicationSource;
export const getLocalSource = (state) => state.localSource;
@@ -340,7 +365,6 @@
export const isLocalTargetKnown = (state) => _.includes(state.databases, state.localTarget);
export const getRemoteTarget = (state) => state.remoteTarget;
-export const isPasswordModalVisible = (state) => state.isPasswordModalVisible;
export const isConflictModalVisible = (state) => state.isConflictModalVisible;
export const getReplicationType = (state) => state.replicationType;
export const getReplicationDocName = (state) => state.replicationDocName;
diff --git a/app/addons/replication/tests/nightwatch/replication.js b/app/addons/replication/tests/nightwatch/replication.js
index bd0c4a4..0d2551f 100644
--- a/app/addons/replication/tests/nightwatch/replication.js
+++ b/app/addons/replication/tests/nightwatch/replication.js
@@ -57,22 +57,33 @@
// enter our source DB
.setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
+ // select source USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-source-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-source-auth-username', waitTime, true)
+
+ // enter source username/password
+ .setValue('#replication-source-auth-password', [password, client.Keys.ENTER])
+
// enter a new target name
.waitForElementVisible('#replication-target', waitTime, true)
.clickWhenVisible('option[value="REPLICATION_TARGET_NEW_LOCAL_DATABASE"]')
.setValue('.replication__new-input', replicatedDBName)
+ // select target USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-target-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-target-auth-username', waitTime, true)
+
+ // enter target username/password
+ .setValue('#replication-target-auth-password', [password, client.Keys.ENTER])
+
.clickWhenVisible('#replicate')
- .waitForElementVisible('.enter-password-modal', waitTime, true)
- .setValue('.enter-password-modal .password-modal-input', password)
- .clickWhenVisible('.enter-password-modal button.save')
- .waitForElementNotPresent('.enter-password-modal', waitTime, true)
.waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
.end();
},
-
'Replicates existing local db to existing local db' : function (client) {
const waitTime = client.globals.maxWaitTime;
const baseUrl = client.globals.test_settings.launch_url;
@@ -100,20 +111,34 @@
.waitForElementVisible('.replication__input-react-select', waitTime, true)
.setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
+ // select source USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-source-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-source-auth-username', waitTime, true)
+
+ // enter source username/password
+ .setValue('#replication-source-auth-password', [password, client.Keys.ENTER])
+
// select existing local as the target
.waitForElementVisible('#replication-target', waitTime, true)
.clickWhenVisible('#replication-target option[value="REPLICATION_TARGET_EXISTING_LOCAL_DATABASE"]')
.setValue('#replication-target-local .Select-input input', [newDatabaseName2, client.Keys.ENTER])
+ // select target USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-target-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-target-auth-username', waitTime, true)
+
+ // enter target username/password
+ .setValue('#replication-target-auth-password', [password, client.Keys.ENTER])
+
.getAttribute('#replicate', 'disabled', function (result) {
// confirm it's not disabled
this.assert.equal(result.value, null);
})
.clickWhenVisible('#replicate')
- .waitForElementVisible('.enter-password-modal', waitTime, true)
- .setValue('.enter-password-modal input[type="password"]', password)
- .clickWhenVisible('.enter-password-modal button.save')
+ .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
.end();
},
@@ -151,23 +176,111 @@
.waitForElementVisible('.replication__input-react-select', waitTime, true)
.setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
+ // select source USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-source-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-source-auth-username', waitTime, true)
+
+ // enter source username/password
+ .setValue('#replication-source-auth-password', [password, client.Keys.ENTER])
+
// select existing local as the target
.waitForElementVisible('#replication-target', waitTime, true)
.clickWhenVisible('#replication-target option[value="REPLICATION_TARGET_EXISTING_LOCAL_DATABASE"]')
.setValue('#replication-target-local .Select-input input', [newDatabaseName2, client.Keys.ENTER])
.setValue('.replication__doc-name-input', [replicatorDoc._id, client.Keys.ENTER])
+ // select target USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-target-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-target-auth-username', waitTime, true)
+
+ // enter target username/password
+ .setValue('#replication-target-auth-password', [password, client.Keys.ENTER])
+
.getAttribute('#replicate', 'disabled', function (result) {
// confirm it's not disabled
this.assert.equal(result.value, null);
})
.clickWhenVisible('#replicate')
+ // confirm overwrite of existing doc
.waitForElementVisible('.replication__error-doc-modal .replication__error-continue', waitTime, true)
.clickWhenVisible('.replication__error-doc-modal .replication__error-continue')
- .waitForElementVisible('.enter-password-modal', waitTime, true)
- .setValue('.enter-password-modal input[type="password"]', password)
- .clickWhenVisible('.enter-password-modal button.save')
+
+ .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
+ .end();
+ },
+
+ 'Show error for missing credentials' : function (client) {
+ const waitTime = client.globals.maxWaitTime;
+ const baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .createDatabase(newDatabaseName1)
+ .checkForDatabaseCreated(newDatabaseName1, waitTime)
+ .createDocument(docName1, newDatabaseName1)
+ .loginToGUI()
+ .url(baseUrl + '/#/replication/_create')
+ .waitForElementVisible('button#replicate', waitTime, true)
+ .waitForElementVisible('#replication-source', waitTime, true)
+
+ // select LOCAL as the source
+ .clickWhenVisible('#replication-source')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('.replication__input-react-select', waitTime, true)
+
+ // enter our source DB
+ .setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
+
+ // enter a new target name
+ .waitForElementVisible('#replication-target', waitTime, true)
+ .clickWhenVisible('option[value="REPLICATION_TARGET_NEW_LOCAL_DATABASE"]')
+ .setValue('.replication__new-input', replicatedDBName)
+
+ .clickWhenVisible('#replicate')
+
+ .waitForElementPresent('.global-notification.alert-error', waitTime, true)
+ .end();
+ },
+
+ 'Show error for invalid credentials' : function (client) {
+ const waitTime = client.globals.maxWaitTime;
+ const baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .createDatabase(newDatabaseName1)
+ .checkForDatabaseCreated(newDatabaseName1, waitTime)
+ .createDocument(docName1, newDatabaseName1)
+ .loginToGUI()
+ .url(baseUrl + '/#/replication/_create')
+ .waitForElementVisible('button#replicate', waitTime, true)
+ .waitForElementVisible('#replication-source', waitTime, true)
+
+ // select LOCAL as the source
+ .clickWhenVisible('#replication-source')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('.replication__input-react-select', waitTime, true)
+
+ // enter our source DB
+ .setValue('.replication__input-react-select .Select-input input', [newDatabaseName1, client.Keys.ENTER])
+
+ // select source USER/PASSWORD authentication
+ .clickWhenVisible('#select-replication-source-auth')
+ .keys(['\uE015', '\uE006'])
+ .waitForElementVisible('#replication-source-auth-username', waitTime, true)
+
+ // enter source username/password
+ .setValue('#replication-source-auth-password', ['wrong_pwd', client.Keys.ENTER])
+
+ // enter a new target name
+ .waitForElementVisible('#replication-target', waitTime, true)
+ .clickWhenVisible('option[value="REPLICATION_TARGET_NEW_REMOTE_DATABASE"]')
+ .setValue('.replication__remote-connection-url', 'http://fake.com/dummydb')
+
+ .clickWhenVisible('#replicate')
+
+ .waitForElementPresent('.global-notification.alert-error', waitTime, true)
.end();
}
};
diff --git a/i18n.json.default.json b/i18n.json.default.json
index f5baf56..d104dcc 100644
--- a/i18n.json.default.json
+++ b/i18n.json.default.json
@@ -13,6 +13,9 @@
"cors-notice": "Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.",
"replication-password-modal-header": "Enter Account Password.",
"replication-password-modal-text": "Replication requires authentication on your credentials.",
+ "replication-user-password-auth-label": "Username and password",
+ "replication-username-input-placeholder": "Username",
+ "replication-password-input-placeholder": "Password",
"auth-missing-credentials": "Username or password cannot be blank.",
"auth-logged-in": "You have been logged in.",
"auth-admin-created": "CouchDB admin created",