Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 899becc

Browse files
authored
Merge pull request #543 from cloudant/542-iam-auth
Add iamApiKey option to ReplicatorBuilder
2 parents 06daca2 + 18f70c0 commit 899becc

File tree

13 files changed

+220
-37
lines changed

13 files changed

+220
-37
lines changed

AndroidTest/app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ dependencies {
8585
compile 'commons-io:commons-io:2.4'
8686
compile 'commons-codec:commons-codec:1.9'
8787
compile 'org.apache.commons:commons-lang3:3.3.2'
88-
compile group: 'com.cloudant', name: 'cloudant-http', version:'2.7.0'
88+
compile group: 'com.cloudant', name: 'cloudant-http', version:'latest.integration'
8989
compile 'com.google.code.findbugs:jsr305:3.0.0' //this is needed for some versions of android
9090
compile files('../../cloudant-sync-datastore-android-encryption/libs/sqlcipher.jar') //required sqlcipher lib
9191
compile files('../../cloudant-sync-datastore-android/libs/android-support-v4.jar')
@@ -108,6 +108,7 @@ dependencies {
108108
repositories {
109109
mavenLocal()
110110
mavenCentral()
111+
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
111112
}
112113

113114
task(uploadFixtures, type: AndroidExec) {

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 2.1.0 (Unreleased)
2+
- [NEW] Added support for authenticating with IAM API keys. See
3+
[README](https://github.com/cloudant/sync-android/blob/2.1.0/README.md) and the
4+
[Bluemix documentation](https://console.bluemix.net/docs/services/Cloudant/guides/iam.html#ibm-cloud-identity-and-access-management)
5+
for more details.
6+
17
# 2.0.2 (2017-06-20)
28
- [FIXED] Removed cloudant-sync-datastore-android project dependency
39
on com.google.android:android. This dependency was inadvertently

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,20 @@ Read more in [the replication docs](https://github.com/cloudant/sync-android/blo
261261
### Authentication
262262

263263
Sync-android uses session cookies by default to authenticate with the server if
264-
credentials are provided with in the URL. If you want more fine-grained control over authentication, provide an interceptor to perform the authentication for each request. For example, if you are using middleware such as [Envoy][envoy], you can use
264+
credentials are provided with in the URL.
265+
266+
To use an IAM API key on IBM Bluemix, use the `iamApiKey` method when
267+
building the `Replicator` object:
268+
269+
```java
270+
Replicator replicator = ReplicatorBuilder.push()
271+
.from(ds)
272+
.to(uri)
273+
.iamApiKey("exampleApiKey")
274+
.build();
275+
```
276+
277+
If you want more fine-grained control over authentication, provide an interceptor to perform the authentication for each request. For example, if you are using middleware such as [Envoy][envoy], you can use
265278
the `BasicAuthInterceptor` to add basic authentication to requests.
266279

267280
Example:

cloudant-sync-datastore-core/build.gradle

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
repositories {
2+
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
3+
}
4+
15
// ************ //
26
// CORE PROJ
37
// ************ //
48
dependencies {
59

6-
compile group: 'com.cloudant', name: 'cloudant-http', version:'2.7.0'
10+
compile group: 'com.cloudant', name: 'cloudant-http', version:'latest.integration'
711
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version:'2.1.1'
812
compile group: 'commons-codec', name: 'commons-codec', version:'1.10'
913
compile group: 'commons-io', name: 'commons-io', version:'2.4'

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/replication/ReplicatorBuilder.java

Lines changed: 95 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
package com.cloudant.sync.replication;
1616

17+
import com.cloudant.http.HttpConnectionInterceptor;
1718
import com.cloudant.http.HttpConnectionRequestInterceptor;
1819
import com.cloudant.http.HttpConnectionResponseInterceptor;
19-
import com.cloudant.http.interceptors.CookieInterceptor;
20+
import com.cloudant.http.internal.interceptors.CookieInterceptor;
21+
import com.cloudant.http.internal.interceptors.IamCookieInterceptor;
2022
import com.cloudant.http.internal.interceptors.UserAgentInterceptor;
2123
import com.cloudant.sync.documentstore.DocumentStore;
2224
import com.cloudant.sync.internal.replication.PullStrategy;
@@ -25,8 +27,10 @@
2527
import com.cloudant.sync.internal.util.Misc;
2628

2729
import java.io.UnsupportedEncodingException;
30+
import java.net.MalformedURLException;
2831
import java.net.URI;
2932
import java.net.URISyntaxException;
33+
import java.net.URL;
3034
import java.net.URLDecoder;
3135
import java.util.ArrayList;
3236
import java.util.Arrays;
@@ -44,24 +48,31 @@ public abstract class ReplicatorBuilder<S, T, E> {
4448
"META-INF/com.cloudant.sync.client.properties");
4549

4650
private T target;
51+
4752
private S source;
53+
4854
private String username;
55+
4956
private String password;
5057

5158
private int id = ReplicatorImpl.NULL_ID;
59+
5260
private List<HttpConnectionRequestInterceptor> requestInterceptors = new ArrayList
5361
<HttpConnectionRequestInterceptor>();
62+
5463
private List<HttpConnectionResponseInterceptor> responseInterceptors = new ArrayList
5564
<HttpConnectionResponseInterceptor>();
5665

66+
private String iamApiKey = null;
67+
5768
private ReplicatorBuilder(){
5869
requestInterceptors.add(USER_AGENT_INTERCEPTOR);
5970
}
6071

61-
private URI addCookieInterceptorIfRequired(URI uri) {
72+
private int getDefaultPort(URI uri) {
73+
6274
String uriProtocol = uri.getScheme();
63-
String uriHost = uri.getHost();
64-
String uriPath = uri.getRawPath();
75+
6576

6677
// assign default port if it hasn't been set
6778
// and check that we support the protocol
@@ -75,6 +86,10 @@ private URI addCookieInterceptorIfRequired(URI uri) {
7586
"Protocol %s not supported", uriProtocol));
7687
}
7788

89+
return uriPort;
90+
}
91+
92+
private void setUserInfo(URI uri) {
7893
if (uri.getUserInfo() != null && this.username == null && this.password == null) {
7994
String[] parts = uri.getRawUserInfo().split(":");
8095
if (parts.length == 2) {
@@ -86,16 +101,34 @@ private URI addCookieInterceptorIfRequired(URI uri) {
86101
throw new RuntimeException(e);
87102
}
88103
}
104+
89105
}
106+
}
90107

108+
private URI scrubUri(URI uri, String uriHost, String uriPath, String uriProtocol, int uriPort) {
91109
if(this.username == null && this.password == null){
92110
return uri;
93111
}
94112

113+
try {
114+
//Remove user credentials from url
115+
return new URI(uriProtocol
116+
+ "://"
117+
+ uriHost
118+
+ ":"
119+
+ uriPort
120+
+ (uriPath != null ? uriPath : ""));
121+
} catch (URISyntaxException use) {
122+
throw new RuntimeException("Failed to construct URI", use);
123+
}
124+
125+
}
126+
127+
private URI getBaseUri(String uriHost, String uriPath, String uriProtocol, int uriPort) {
95128
try {
96129
String path = uriPath == null ? "" : uriPath;
97130

98-
if(path.length() > 0) {
131+
if (path.length() > 0) {
99132
int index = path.lastIndexOf("/");
100133
if (index == path.length() - 1) {
101134
// we need to go back one
@@ -105,27 +138,43 @@ private URI addCookieInterceptorIfRequired(URI uri) {
105138
path = path.substring(0, index);
106139
}
107140

108-
URI baseURI = new URI(uriProtocol, null, uriHost, uriPort, path, null, null);
109-
CookieInterceptor ci = new CookieInterceptor(this.username, this.password, baseURI.toString());
110-
requestInterceptors.add(ci);
111-
responseInterceptors.add(ci);
141+
return new URI(uriProtocol, null, uriHost, uriPort, path, null, null);
112142
} catch (URISyntaxException e) {
113143
throw new RuntimeException(e);
114144
}
145+
}
115146

116-
try {
117-
//Remove user credentials from url
118-
return new URI(uriProtocol
119-
+ "://"
120-
+ uriHost
121-
+ ":"
122-
+ uriPort
123-
+ (uriPath != null ? uriPath : ""));
124-
} catch (URISyntaxException use) {
125-
throw new RuntimeException("Failed to construct URI", use);
147+
private void setAuthInterceptor(String uriHost, String uriPath, String uriProtocol, int uriPort) {
148+
if (iamApiKey != null) {
149+
URI baseURI = getBaseUri(uriHost, uriPath, uriProtocol, uriPort);
150+
IamCookieInterceptor ici = new IamCookieInterceptor(iamApiKey, baseURI.toString());
151+
requestInterceptors.add(ici);
152+
responseInterceptors.add(ici);
153+
154+
} else if (this.username != null && this.password != null) {
155+
URI baseURI = getBaseUri(uriHost, uriPath, uriProtocol, uriPort);
156+
CookieInterceptor ci = new CookieInterceptor(this.username, this.password, baseURI.toString());
157+
requestInterceptors.add(ci);
158+
responseInterceptors.add(ci);
126159
}
127160
}
128161

162+
// - set default port if needed and validate protocol (http(s))
163+
// - scrub out username and password if given, and pass it into cookie interceptor or discard it if we're doing IAM
164+
// - add IAM interceptor if needed
165+
// - else add cookie interceptor if needed
166+
private URI addAuthInterceptorIfRequired(URI uri) {
167+
168+
String uriProtocol = uri.getScheme();
169+
String uriHost = uri.getHost();
170+
String uriPath = uri.getRawPath();
171+
172+
int uriPort = getDefaultPort(uri);
173+
setUserInfo(uri);
174+
setAuthInterceptor(uriHost, uriPath, uriProtocol, uriPort);
175+
return scrubUri(uri, uriHost, uriPath, uriProtocol, uriPort);
176+
}
177+
129178
/**
130179
* A Push Replication Builder
131180
*/
@@ -146,7 +195,7 @@ public Replicator build() {
146195
"Source and target cannot be null");
147196

148197
// add cookie interceptor and remove creds from URI if required
149-
super.target = super.addCookieInterceptorIfRequired(super.target);
198+
super.target = super.addAuthInterceptorIfRequired(super.target);
150199

151200
PushStrategy pushStrategy = new PushStrategy(super.source.database(),
152201
super.target,
@@ -203,6 +252,7 @@ public Push pushAttachmentsInline(PushAttachmentsInline pushAttachmentsInline) {
203252
this.pushAttachmentsInline = pushAttachmentsInline;
204253
return this;
205254
}
255+
206256
}
207257

208258
/**
@@ -225,7 +275,7 @@ public Replicator build() {
225275
"Source and target cannot be null");
226276

227277
// add cookie interceptor and remove creds from URI if required
228-
super.source = super.addCookieInterceptorIfRequired(super.source);
278+
super.source = super.addAuthInterceptorIfRequired(super.source);
229279

230280
PullStrategy pullStrategy = new PullStrategy(super.source,
231281
super.target.database(),
@@ -316,6 +366,30 @@ public E withId(int id) {
316366
return (E) this;
317367
}
318368

369+
/**
370+
* <p>
371+
* Sets the IAM API key to use for authenticating requests.
372+
* </p>
373+
* <p>
374+
* Note: the replicator will only use IAM to authenticate requests. This means that the
375+
* userinfo part of the URL, if set, will be ignored.
376+
* </p>
377+
* <p>
378+
* See the
379+
* <a href="https://console.bluemix.net/docs/services/Cloudant/guides/iam.html#ibm-cloud-identity-and-access-management" target="_blank">
380+
* Bluemix Identity and Access Management
381+
* </a> documentation for more details.
382+
* </p>
383+
*
384+
* @param iamApiKey The API key
385+
* @return This instance of {@link ReplicatorBuilder}
386+
*/
387+
public E iamApiKey(String iamApiKey) {
388+
this.iamApiKey = iamApiKey;
389+
//noinspection unchecked
390+
return (E) this;
391+
}
392+
319393
/**
320394
* Variable argument version of {@link #addRequestInterceptors(List)}
321395
* @param interceptors The request interceptors to add.

cloudant-sync-datastore-core/src/test/java/com/cloudant/common/TestOptions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,6 @@ public class TestOptions {
102102
public static final Boolean IGNORE_AUTH_HEADERS = Boolean.valueOf(
103103
System.getProperty("test.couch.ignore.auth.headers", Boolean.TRUE.toString()));
104104

105+
public static final String COUCH_IAM_API_KEY = System.getProperty("test.couch.iam.api.key");
106+
105107
}

cloudant-sync-datastore-core/src/test/java/com/cloudant/http/HttpTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import com.cloudant.common.CouchTestBase;
1818
import com.cloudant.common.RequireRunningCouchDB;
1919
import com.cloudant.common.TestOptions;
20-
import com.cloudant.http.interceptors.CookieInterceptor;
20+
import com.cloudant.http.internal.interceptors.CookieInterceptor;
2121
import com.cloudant.sync.internal.mazha.CouchClient;
2222
import com.cloudant.sync.internal.mazha.CouchConfig;
2323
import com.cloudant.sync.internal.util.JSONUtils;

cloudant-sync-datastore-core/src/test/java/com/cloudant/sync/internal/mazha/CouchConfig.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020

2121
package com.cloudant.sync.internal.mazha;
2222

23+
import com.cloudant.common.TestOptions;
2324
import com.cloudant.http.HttpConnectionRequestInterceptor;
2425
import com.cloudant.http.HttpConnectionResponseInterceptor;
25-
import com.cloudant.http.interceptors.CookieInterceptor;
26+
import com.cloudant.http.internal.interceptors.CookieInterceptor;
27+
import com.cloudant.http.internal.interceptors.IamCookieInterceptor;
2628

29+
import java.net.MalformedURLException;
2730
import java.net.URI;
2831
import java.net.URISyntaxException;
32+
import java.net.URL;
2933
import java.util.ArrayList;
3034
import java.util.Collections;
3135
import java.util.List;
@@ -52,8 +56,8 @@ public class CouchConfig {
5256

5357
public CouchConfig(URI rootUri) {
5458
this(rootUri,
55-
Collections.<HttpConnectionRequestInterceptor>emptyList(),
56-
Collections.<HttpConnectionResponseInterceptor>emptyList(),
59+
new ArrayList<HttpConnectionRequestInterceptor>(),
60+
new ArrayList<HttpConnectionResponseInterceptor>(),
5761
null,
5862
null);
5963
}
@@ -68,10 +72,16 @@ public CouchConfig(URI rootUri,
6872
this.responseInterceptors = responseInterceptors;
6973
this.username = username;
7074
this.password = password;
71-
75+
if (TestOptions.COUCH_IAM_API_KEY != null) {
76+
int slash = this.rootUri.toString().lastIndexOf("/");
77+
String root = this.rootUri.toString().substring(0, slash);
78+
IamCookieInterceptor ici = new IamCookieInterceptor(TestOptions.COUCH_IAM_API_KEY,
79+
root);
80+
this.requestInterceptors.add(ici);
81+
this.responseInterceptors.add(ici);
82+
}
7283
}
7384

74-
7585
public List<HttpConnectionRequestInterceptor> getRequestInterceptors(boolean includeCookie) {
7686
CookieInterceptor cookieInterceptor = buildCookieInterceptor();
7787
if (includeCookie && cookieInterceptor != null) {

cloudant-sync-datastore-core/src/test/java/com/cloudant/sync/internal/mazha/SpecifiedCouch.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static com.cloudant.common.TestOptions.COUCH_USERNAME;
2323
import static com.cloudant.common.TestOptions.HTTP_PROTOCOL;
2424

25-
import com.cloudant.http.interceptors.CookieInterceptor;
2625
import com.cloudant.http.HttpConnectionRequestInterceptor;
2726
import com.cloudant.http.HttpConnectionResponseInterceptor;
2827

@@ -53,8 +52,8 @@ public static CouchConfig defaultConfig(String dbName){
5352
uriString = String.format("%s://%s:%s/%s", HTTP_PROTOCOL, COUCH_HOST, COUCH_PORT, dbName);
5453
}
5554
CouchConfig config = new CouchConfig(new URI(uriString),
56-
Collections.<HttpConnectionRequestInterceptor>emptyList(),
57-
Collections.<HttpConnectionResponseInterceptor>emptyList(),
55+
new ArrayList<HttpConnectionRequestInterceptor>(),
56+
new ArrayList<HttpConnectionResponseInterceptor>(),
5857
COUCH_USERNAME,
5958
COUCH_PASSWORD);
6059
return config;

cloudant-sync-datastore-core/src/test/java/com/cloudant/sync/internal/replication/CompactedDBReplicationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public void replicationFromCompactedDB() throws Exception{
5656
// skip test if we are doing cookie auth, we don't have the interceptor chain to do it
5757
// when we call ClientTestUtils.executeHttpPostRequest
5858
if(TestOptions.COOKIE_AUTH){return;}
59+
if(TestOptions.COUCH_IAM_API_KEY != null){return;}
5960

6061
String documentName;
6162
Bar bar = BarUtils.createBar(remoteDb, "Bob", 12);

0 commit comments

Comments
 (0)