Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.

Commit 837dca2

Browse files
Move allowed callers and skill conversation factory to SDK (#1062)
* Move allowed callers and skill convo factory * Update for Application file. Co-authored-by: tracyboehrer <tracyboehrer@users.noreply.github.com>
1 parent 466fd90 commit 837dca2

File tree

13 files changed

+332
-204
lines changed

13 files changed

+332
-204
lines changed

samples/81.skills-skilldialog/dialog-root-bot/src/main/java/com/microsoft/bot/sample/dialogrootbot/SkillConversationIdFactory.java renamed to libraries/bot-builder/src/main/java/com/microsoft/bot/builder/skills/SkillConversationIdFactory.java

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MT License.
33

4-
package com.microsoft.bot.sample.dialogrootbot;
4+
package com.microsoft.bot.builder.skills;
55

66
import java.util.HashMap;
77
import java.util.Map;
88
import java.util.concurrent.CompletableFuture;
99

1010
import com.microsoft.bot.builder.Storage;
11-
import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase;
12-
import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions;
13-
import com.microsoft.bot.builder.skills.SkillConversationReference;
1411
import com.microsoft.bot.connector.Async;
1512
import com.microsoft.bot.schema.ConversationReference;
1613

@@ -25,35 +22,59 @@ public class SkillConversationIdFactory extends SkillConversationIdFactoryBase {
2522

2623
private Storage storage;
2724

25+
/**
26+
* Creates an instance of a SkillConversationIdFactory.
27+
*
28+
* @param storage A storage instance for the factory.
29+
*/
2830
public SkillConversationIdFactory(Storage storage) {
2931
if (storage == null) {
3032
throw new IllegalArgumentException("Storage cannot be null.");
3133
}
3234
this.storage = storage;
3335
}
3436

37+
/**
38+
* Creates a conversation id for a skill conversation.
39+
*
40+
* @param options A {@link SkillConversationIdFactoryOptions} instance
41+
* containing parameters for creating the conversation ID.
42+
*
43+
* @return A unique conversation ID used to communicate with the skill.
44+
*
45+
* It should be possible to use the returned String on a request URL and
46+
* it should not contain special characters.
47+
*/
3548
@Override
3649
public CompletableFuture<String> createSkillConversationId(SkillConversationIdFactoryOptions options) {
3750
if (options == null) {
3851
Async.completeExceptionally(new IllegalArgumentException("options cannot be null."));
3952
}
4053
ConversationReference conversationReference = options.getActivity().getConversationReference();
41-
String skillConversationId = String.format(
42-
"%s-%s-%s-skillconvo",
43-
conversationReference.getConversation().getId(),
44-
options.getBotFrameworkSkill().getId(),
45-
conversationReference.getChannelId()
46-
);
54+
String skillConversationId = String.format("%s-%s-%s-skillconvo",
55+
conversationReference.getConversation().getId(), options.getBotFrameworkSkill().getId(),
56+
conversationReference.getChannelId());
4757

4858
SkillConversationReference skillConversationReference = new SkillConversationReference();
4959
skillConversationReference.setConversationReference(conversationReference);
5060
skillConversationReference.setOAuthScope(options.getFromBotOAuthScope());
5161
Map<String, Object> skillConversationInfo = new HashMap<String, Object>();
5262
skillConversationInfo.put(skillConversationId, skillConversationReference);
5363
return storage.write(skillConversationInfo)
54-
.thenCompose(result -> CompletableFuture.completedFuture(skillConversationId));
64+
.thenCompose(result -> CompletableFuture.completedFuture(skillConversationId));
5565
}
5666

67+
/**
68+
* Gets the {@link SkillConversationReference} created using
69+
* {@link SkillConversationIdFactory#createSkillConversationId} for a
70+
* skillConversationId.
71+
*
72+
* @param skillConversationId A skill conversationId created using
73+
* {@link SkillConversationIdFactory#createSkillConversationId}.
74+
*
75+
* @return The caller's {@link ConversationReference} for a skillConversationId.
76+
* null if not found.
77+
*/
5778
@Override
5879
public CompletableFuture<SkillConversationReference> getSkillConversationReference(String skillConversationId) {
5980
if (StringUtils.isAllBlank(skillConversationId)) {
@@ -63,13 +84,22 @@ public CompletableFuture<SkillConversationReference> getSkillConversationReferen
6384
return storage.read(new String[] {skillConversationId}).thenCompose(skillConversationInfo -> {
6485
if (skillConversationInfo.size() > 0) {
6586
return CompletableFuture
66-
.completedFuture((SkillConversationReference) skillConversationInfo.get(skillConversationId));
87+
.completedFuture((SkillConversationReference) skillConversationInfo.get(skillConversationId));
6788
} else {
6889
return CompletableFuture.completedFuture(null);
6990
}
7091
});
7192
}
7293

94+
/**
95+
* Deletes a {@link ConversationReference} .
96+
*
97+
* @param skillConversationId A skill conversationId created using {@link
98+
* CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT
99+
* reading()#getCancellationToken())} .
100+
*
101+
* @return A {@link CompletableFuture} representing the asynchronous operation.
102+
*/
73103
@Override
74104
public CompletableFuture<Void> deleteConversationReference(String skillConversationId) {
75105
return storage.delete(new String[] {skillConversationId});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MT License.
3+
4+
package com.microsoft.bot.builder;
5+
6+
import java.net.URI;
7+
import java.net.URISyntaxException;
8+
import java.util.UUID;
9+
10+
import com.microsoft.bot.builder.skills.BotFrameworkSkill;
11+
import com.microsoft.bot.builder.skills.SkillConversationIdFactory;
12+
import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions;
13+
import com.microsoft.bot.builder.skills.SkillConversationReference;
14+
import com.microsoft.bot.schema.Activity;
15+
import com.microsoft.bot.schema.ConversationAccount;
16+
import com.microsoft.bot.schema.ConversationReference;
17+
18+
import org.junit.Test;
19+
import org.apache.commons.lang3.StringUtils;
20+
import org.junit.Assert;
21+
22+
23+
public class SkillConversationIdFactoryTests {
24+
25+
private static final String SERVICE_URL = "http://testbot.com/api/messages";
26+
private final String skillId = "skill";
27+
28+
private final SkillConversationIdFactory skillConversationIdFactory =
29+
new SkillConversationIdFactory(new MemoryStorage());
30+
private final String applicationId = UUID.randomUUID().toString();
31+
private final String botId = UUID.randomUUID().toString();
32+
33+
@Test
34+
public void SkillConversationIdFactoryHappyPath() {
35+
ConversationReference conversationReference = buildConversationReference();
36+
37+
// Create skill conversation
38+
SkillConversationIdFactoryOptions options = new SkillConversationIdFactoryOptions();
39+
options.setActivity(buildMessageActivity(conversationReference));
40+
options.setBotFrameworkSkill(this.buildBotFrameworkSkill());
41+
options.setFromBotId(botId);
42+
options.setFromBotOAuthScope(botId);
43+
44+
45+
String skillConversationId = skillConversationIdFactory.createSkillConversationId(options).join();
46+
47+
Assert.assertFalse(StringUtils.isBlank(skillConversationId));
48+
49+
// Retrieve skill conversation
50+
SkillConversationReference retrievedConversationReference =
51+
skillConversationIdFactory.getSkillConversationReference(skillConversationId).join();
52+
53+
// Delete
54+
skillConversationIdFactory.deleteConversationReference(skillConversationId);
55+
56+
// Retrieve again
57+
SkillConversationReference deletedConversationReference =
58+
skillConversationIdFactory.getSkillConversationReference(skillConversationId).join();
59+
60+
Assert.assertNotNull(retrievedConversationReference);
61+
Assert.assertNotNull(retrievedConversationReference.getConversationReference());
62+
Assert.assertTrue(compareConversationReferences(conversationReference,
63+
retrievedConversationReference.getConversationReference()));
64+
Assert.assertNull(deletedConversationReference);
65+
}
66+
67+
private static ConversationReference buildConversationReference() {
68+
ConversationReference conversationReference = new ConversationReference();
69+
conversationReference.setConversation(new ConversationAccount(UUID.randomUUID().toString()));
70+
conversationReference.setServiceUrl(SERVICE_URL);
71+
return conversationReference;
72+
}
73+
74+
private static Activity buildMessageActivity(ConversationReference conversationReference) {
75+
if (conversationReference == null) {
76+
throw new IllegalArgumentException("conversationReference cannot be null.");
77+
}
78+
79+
Activity activity = Activity.createMessageActivity();
80+
activity.applyConversationReference(conversationReference);
81+
82+
return activity;
83+
}
84+
85+
private BotFrameworkSkill buildBotFrameworkSkill() {
86+
BotFrameworkSkill skill = new BotFrameworkSkill();
87+
skill.setAppId(applicationId);
88+
skill.setId(skillId);
89+
try {
90+
skill.setSkillEndpoint(new URI(SERVICE_URL));
91+
} catch (URISyntaxException e) {
92+
e.printStackTrace();
93+
}
94+
return skill;
95+
}
96+
97+
private static boolean compareConversationReferences(
98+
ConversationReference reference1,
99+
ConversationReference reference2
100+
) {
101+
return reference1.getConversation().getId() == reference2.getConversation().getId()
102+
&& reference1.getServiceUrl() == reference2.getServiceUrl();
103+
}
104+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MT License.
3+
4+
package com.microsoft.bot.connector.authentication;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.concurrent.CompletableFuture;
10+
11+
import com.microsoft.bot.connector.Async;
12+
13+
/**
14+
* Sample claims validator that loads an allowed list from configuration if
15+
* presentand checks that requests are coming from allowed parent bots.
16+
*/
17+
public class AllowedCallersClaimsValidator extends ClaimsValidator {
18+
19+
private List<String> allowedCallers;
20+
21+
/**
22+
* Creates an instance of an {@link AllowedCallersClaimsValidator}.
23+
* @param withAllowedCallers A List<String> that contains the list of allowed callers.
24+
*/
25+
public AllowedCallersClaimsValidator(List<String> withAllowedCallers) {
26+
this.allowedCallers = withAllowedCallers != null ? withAllowedCallers : new ArrayList<String>();
27+
}
28+
29+
/**
30+
* Validates a Map of claims and should throw an exception if the
31+
* validation fails.
32+
*
33+
* @param claims The Map of claims to validate.
34+
*
35+
* @return true if the validation is successful, false if not.
36+
*/
37+
@Override
38+
public CompletableFuture<Void> validateClaims(Map<String, String> claims) {
39+
if (claims == null) {
40+
return Async.completeExceptionally(new IllegalArgumentException("Claims cannot be null"));
41+
}
42+
43+
// If _allowedCallers contains an "*", we allow all callers.
44+
if (SkillValidation.isSkillClaim(claims) && !allowedCallers.contains("*")) {
45+
// Check that the appId claim in the skill request instanceof in the list of
46+
// callers configured for this bot.
47+
String appId = JwtTokenValidation.getAppIdFromClaims(claims);
48+
if (!allowedCallers.contains(appId)) {
49+
return Async.completeExceptionally(
50+
new RuntimeException(
51+
String.format(
52+
"Received a request from a bot with an app ID of \"%s\". To enable requests from this "
53+
+ "caller, add the app ID to the configured set of allowedCallers.",
54+
appId
55+
)
56+
)
57+
);
58+
}
59+
}
60+
61+
return CompletableFuture.completedFuture(null);
62+
}
63+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MT License.
3+
4+
package com.microsoft.bot.connector;
5+
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.UUID;
12+
13+
import com.microsoft.bot.connector.authentication.AllowedCallersClaimsValidator;
14+
import com.microsoft.bot.connector.authentication.AuthenticationConstants;
15+
16+
import org.apache.commons.lang3.tuple.Pair;
17+
import org.junit.Assert;
18+
import org.junit.Test;
19+
20+
public class AllowedCallersClaimsValidationTests {
21+
22+
private final String version = "1.0";
23+
24+
private final String audienceClaim = UUID.randomUUID().toString();
25+
26+
public static List<Pair<String, List<String>>> getConfigureServicesSucceedsData() {
27+
String primaryAppId = UUID.randomUUID().toString();
28+
String secondaryAppId = UUID.randomUUID().toString();
29+
30+
List<Pair<String, List<String>>> resultList = new ArrayList<Pair<String, List<String>>>();
31+
// Null allowed callers
32+
resultList.add(Pair.of(null, null));
33+
// Null configuration with attempted caller
34+
resultList.add(Pair.of(primaryAppId, null));
35+
// Empty allowed callers array
36+
resultList.add(Pair.of(null, new ArrayList<String>()));
37+
// Allow any caller
38+
resultList.add(Pair.of(primaryAppId, new ArrayList<String>() { { add("*"); } }));
39+
// Specify allowed caller
40+
resultList.add((Pair.of(primaryAppId, new ArrayList<String>() { { add(primaryAppId); } })));
41+
// Specify multiple callers
42+
resultList.add((Pair.of(primaryAppId, new ArrayList<String>() { { add(primaryAppId);
43+
add(secondaryAppId); } })));
44+
// Blocked caller throws exception
45+
resultList.add((Pair.of(primaryAppId, new ArrayList<String>() { { add(secondaryAppId); } })));
46+
return resultList;
47+
}
48+
49+
@Test
50+
public void TestAcceptAllowedCallersArray() {
51+
List<Pair<String, List<String>>> configuredServices = getConfigureServicesSucceedsData();
52+
for (Pair<String, List<String>> item : configuredServices) {
53+
acceptAllowedCallersArray(item.getLeft(), item.getRight());
54+
}
55+
}
56+
57+
58+
public void acceptAllowedCallersArray(String allowedCallerClaimId, List<String> allowList) {
59+
AllowedCallersClaimsValidator validator = new AllowedCallersClaimsValidator(allowList);
60+
61+
if (allowedCallerClaimId != null) {
62+
Map<String, String> claims = createCallerClaims(allowedCallerClaimId);
63+
64+
if (allowList != null) {
65+
if (allowList.contains(allowedCallerClaimId) || allowList.contains("*")) {
66+
validator.validateClaims(claims);
67+
} else {
68+
validateUnauthorizedAccessException(allowedCallerClaimId, validator, claims);
69+
}
70+
} else {
71+
validateUnauthorizedAccessException(allowedCallerClaimId, validator, claims);
72+
}
73+
}
74+
}
75+
76+
private static void validateUnauthorizedAccessException(
77+
String allowedCallerClaimId,
78+
AllowedCallersClaimsValidator validator,
79+
Map<String, String> claims) {
80+
try {
81+
validator.validateClaims(claims);
82+
} catch (RuntimeException exception) {
83+
Assert.assertTrue(exception.getMessage().contains(allowedCallerClaimId));
84+
}
85+
}
86+
87+
private Map<String, String> createCallerClaims(String appId) {
88+
Map<String, String> callerClaimMap = new HashMap<String, String>();
89+
90+
callerClaimMap.put(AuthenticationConstants.APPID_CLAIM, appId);
91+
callerClaimMap.put(AuthenticationConstants.VERSION_CLAIM, version);
92+
callerClaimMap.put(AuthenticationConstants.AUDIENCE_CLAIM, audienceClaim);
93+
return callerClaimMap;
94+
}
95+
}
96+

samples/80.skills-simple-bot-to-bot/DialogRootBot/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@
8484
<version>4.6.0-preview9</version>
8585
<scope>compile</scope>
8686
</dependency>
87+
<dependency>
88+
<groupId>com.microsoft.bot</groupId>
89+
<artifactId>bot-builder</artifactId>
90+
<version>4.6.0-preview9</version>
91+
<scope>compile</scope>
92+
</dependency>
8793
</dependencies>
8894

8995
<profiles>

0 commit comments

Comments
 (0)