Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MT License.

package com.microsoft.bot.sample.dialogrootbot;
package com.microsoft.bot.builder.skills;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import com.microsoft.bot.builder.Storage;
import com.microsoft.bot.builder.skills.SkillConversationIdFactoryBase;
import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions;
import com.microsoft.bot.builder.skills.SkillConversationReference;
import com.microsoft.bot.connector.Async;
import com.microsoft.bot.schema.ConversationReference;

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

private Storage storage;

/**
* Creates an instance of a SkillConversationIdFactory.
*
* @param storage A storage instance for the factory.
*/
public SkillConversationIdFactory(Storage storage) {
if (storage == null) {
throw new IllegalArgumentException("Storage cannot be null.");
}
this.storage = storage;
}

/**
* Creates a conversation id for a skill conversation.
*
* @param options A {@link SkillConversationIdFactoryOptions} instance
* containing parameters for creating the conversation ID.
*
* @return A unique conversation ID used to communicate with the skill.
*
* It should be possible to use the returned String on a request URL and
* it should not contain special characters.
*/
@Override
public CompletableFuture<String> createSkillConversationId(SkillConversationIdFactoryOptions options) {
if (options == null) {
Async.completeExceptionally(new IllegalArgumentException("options cannot be null."));
}
ConversationReference conversationReference = options.getActivity().getConversationReference();
String skillConversationId = String.format(
"%s-%s-%s-skillconvo",
conversationReference.getConversation().getId(),
options.getBotFrameworkSkill().getId(),
conversationReference.getChannelId()
);
String skillConversationId = String.format("%s-%s-%s-skillconvo",
conversationReference.getConversation().getId(), options.getBotFrameworkSkill().getId(),
conversationReference.getChannelId());

SkillConversationReference skillConversationReference = new SkillConversationReference();
skillConversationReference.setConversationReference(conversationReference);
skillConversationReference.setOAuthScope(options.getFromBotOAuthScope());
Map<String, Object> skillConversationInfo = new HashMap<String, Object>();
skillConversationInfo.put(skillConversationId, skillConversationReference);
return storage.write(skillConversationInfo)
.thenCompose(result -> CompletableFuture.completedFuture(skillConversationId));
.thenCompose(result -> CompletableFuture.completedFuture(skillConversationId));
}

/**
* Gets the {@link SkillConversationReference} created using
* {@link SkillConversationIdFactory#createSkillConversationId} for a
* skillConversationId.
*
* @param skillConversationId A skill conversationId created using
* {@link SkillConversationIdFactory#createSkillConversationId}.
*
* @return The caller's {@link ConversationReference} for a skillConversationId.
* null if not found.
*/
@Override
public CompletableFuture<SkillConversationReference> getSkillConversationReference(String skillConversationId) {
if (StringUtils.isAllBlank(skillConversationId)) {
Expand All @@ -63,13 +84,22 @@ public CompletableFuture<SkillConversationReference> getSkillConversationReferen
return storage.read(new String[] {skillConversationId}).thenCompose(skillConversationInfo -> {
if (skillConversationInfo.size() > 0) {
return CompletableFuture
.completedFuture((SkillConversationReference) skillConversationInfo.get(skillConversationId));
.completedFuture((SkillConversationReference) skillConversationInfo.get(skillConversationId));
} else {
return CompletableFuture.completedFuture(null);
}
});
}

/**
* Deletes a {@link ConversationReference} .
*
* @param skillConversationId A skill conversationId created using {@link
* CreateSkillConversationId(SkillConversationIdFactoryOptions,System#getT
* reading()#getCancellationToken())} .
*
* @return A {@link CompletableFuture} representing the asynchronous operation.
*/
@Override
public CompletableFuture<Void> deleteConversationReference(String skillConversationId) {
return storage.delete(new String[] {skillConversationId});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MT License.

package com.microsoft.bot.builder;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.UUID;

import com.microsoft.bot.builder.skills.BotFrameworkSkill;
import com.microsoft.bot.builder.skills.SkillConversationIdFactory;
import com.microsoft.bot.builder.skills.SkillConversationIdFactoryOptions;
import com.microsoft.bot.builder.skills.SkillConversationReference;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ConversationAccount;
import com.microsoft.bot.schema.ConversationReference;

import org.junit.Test;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;


public class SkillConversationIdFactoryTests {

private static final String SERVICE_URL = "http://testbot.com/api/messages";
private final String skillId = "skill";

private final SkillConversationIdFactory skillConversationIdFactory =
new SkillConversationIdFactory(new MemoryStorage());
private final String applicationId = UUID.randomUUID().toString();
private final String botId = UUID.randomUUID().toString();

@Test
public void SkillConversationIdFactoryHappyPath() {
ConversationReference conversationReference = buildConversationReference();

// Create skill conversation
SkillConversationIdFactoryOptions options = new SkillConversationIdFactoryOptions();
options.setActivity(buildMessageActivity(conversationReference));
options.setBotFrameworkSkill(this.buildBotFrameworkSkill());
options.setFromBotId(botId);
options.setFromBotOAuthScope(botId);


String skillConversationId = skillConversationIdFactory.createSkillConversationId(options).join();

Assert.assertFalse(StringUtils.isBlank(skillConversationId));

// Retrieve skill conversation
SkillConversationReference retrievedConversationReference =
skillConversationIdFactory.getSkillConversationReference(skillConversationId).join();

// Delete
skillConversationIdFactory.deleteConversationReference(skillConversationId);

// Retrieve again
SkillConversationReference deletedConversationReference =
skillConversationIdFactory.getSkillConversationReference(skillConversationId).join();

Assert.assertNotNull(retrievedConversationReference);
Assert.assertNotNull(retrievedConversationReference.getConversationReference());
Assert.assertTrue(compareConversationReferences(conversationReference,
retrievedConversationReference.getConversationReference()));
Assert.assertNull(deletedConversationReference);
}

private static ConversationReference buildConversationReference() {
ConversationReference conversationReference = new ConversationReference();
conversationReference.setConversation(new ConversationAccount(UUID.randomUUID().toString()));
conversationReference.setServiceUrl(SERVICE_URL);
return conversationReference;
}

private static Activity buildMessageActivity(ConversationReference conversationReference) {
if (conversationReference == null) {
throw new IllegalArgumentException("conversationReference cannot be null.");
}

Activity activity = Activity.createMessageActivity();
activity.applyConversationReference(conversationReference);

return activity;
}

private BotFrameworkSkill buildBotFrameworkSkill() {
BotFrameworkSkill skill = new BotFrameworkSkill();
skill.setAppId(applicationId);
skill.setId(skillId);
try {
skill.setSkillEndpoint(new URI(SERVICE_URL));
} catch (URISyntaxException e) {
e.printStackTrace();
}
return skill;
}

private static boolean compareConversationReferences(
ConversationReference reference1,
ConversationReference reference2
) {
return reference1.getConversation().getId() == reference2.getConversation().getId()
&& reference1.getServiceUrl() == reference2.getServiceUrl();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MT License.

package com.microsoft.bot.connector.authentication;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import com.microsoft.bot.connector.Async;

/**
* Sample claims validator that loads an allowed list from configuration if
* presentand checks that requests are coming from allowed parent bots.
*/
public class AllowedCallersClaimsValidator extends ClaimsValidator {

private List<String> allowedCallers;

/**
* Creates an instance of an {@link AllowedCallersClaimsValidator}.
* @param withAllowedCallers A List<String> that contains the list of allowed callers.
*/
public AllowedCallersClaimsValidator(List<String> withAllowedCallers) {
this.allowedCallers = withAllowedCallers != null ? withAllowedCallers : new ArrayList<String>();
}

/**
* Validates a Map of claims and should throw an exception if the
* validation fails.
*
* @param claims The Map of claims to validate.
*
* @return true if the validation is successful, false if not.
*/
@Override
public CompletableFuture<Void> validateClaims(Map<String, String> claims) {
if (claims == null) {
return Async.completeExceptionally(new IllegalArgumentException("Claims cannot be null"));
}

// If _allowedCallers contains an "*", we allow all callers.
if (SkillValidation.isSkillClaim(claims) && !allowedCallers.contains("*")) {
// Check that the appId claim in the skill request instanceof in the list of
// callers configured for this bot.
String appId = JwtTokenValidation.getAppIdFromClaims(claims);
if (!allowedCallers.contains(appId)) {
return Async.completeExceptionally(
new RuntimeException(
String.format(
"Received a request from a bot with an app ID of \"%s\". To enable requests from this "
+ "caller, add the app ID to the configured set of allowedCallers.",
appId
)
)
);
}
}

return CompletableFuture.completedFuture(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MT License.

package com.microsoft.bot.connector;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.microsoft.bot.connector.authentication.AllowedCallersClaimsValidator;
import com.microsoft.bot.connector.authentication.AuthenticationConstants;

import org.apache.commons.lang3.tuple.Pair;
import org.junit.Assert;
import org.junit.Test;

public class AllowedCallersClaimsValidationTests {

private final String version = "1.0";

private final String audienceClaim = UUID.randomUUID().toString();

public static List<Pair<String, List<String>>> getConfigureServicesSucceedsData() {
String primaryAppId = UUID.randomUUID().toString();
String secondaryAppId = UUID.randomUUID().toString();

List<Pair<String, List<String>>> resultList = new ArrayList<Pair<String, List<String>>>();
// Null allowed callers
resultList.add(Pair.of(null, null));
// Null configuration with attempted caller
resultList.add(Pair.of(primaryAppId, null));
// Empty allowed callers array
resultList.add(Pair.of(null, new ArrayList<String>()));
// Allow any caller
resultList.add(Pair.of(primaryAppId, new ArrayList<String>() { { add("*"); } }));
// Specify allowed caller
resultList.add((Pair.of(primaryAppId, new ArrayList<String>() { { add(primaryAppId); } })));
// Specify multiple callers
resultList.add((Pair.of(primaryAppId, new ArrayList<String>() { { add(primaryAppId);
add(secondaryAppId); } })));
// Blocked caller throws exception
resultList.add((Pair.of(primaryAppId, new ArrayList<String>() { { add(secondaryAppId); } })));
return resultList;
}

@Test
public void TestAcceptAllowedCallersArray() {
List<Pair<String, List<String>>> configuredServices = getConfigureServicesSucceedsData();
for (Pair<String, List<String>> item : configuredServices) {
acceptAllowedCallersArray(item.getLeft(), item.getRight());
}
}


public void acceptAllowedCallersArray(String allowedCallerClaimId, List<String> allowList) {
AllowedCallersClaimsValidator validator = new AllowedCallersClaimsValidator(allowList);

if (allowedCallerClaimId != null) {
Map<String, String> claims = createCallerClaims(allowedCallerClaimId);

if (allowList != null) {
if (allowList.contains(allowedCallerClaimId) || allowList.contains("*")) {
validator.validateClaims(claims);
} else {
validateUnauthorizedAccessException(allowedCallerClaimId, validator, claims);
}
} else {
validateUnauthorizedAccessException(allowedCallerClaimId, validator, claims);
}
}
}

private static void validateUnauthorizedAccessException(
String allowedCallerClaimId,
AllowedCallersClaimsValidator validator,
Map<String, String> claims) {
try {
validator.validateClaims(claims);
} catch (RuntimeException exception) {
Assert.assertTrue(exception.getMessage().contains(allowedCallerClaimId));
}
}

private Map<String, String> createCallerClaims(String appId) {
Map<String, String> callerClaimMap = new HashMap<String, String>();

callerClaimMap.put(AuthenticationConstants.APPID_CLAIM, appId);
callerClaimMap.put(AuthenticationConstants.VERSION_CLAIM, version);
callerClaimMap.put(AuthenticationConstants.AUDIENCE_CLAIM, audienceClaim);
return callerClaimMap;
}
}

6 changes: 6 additions & 0 deletions samples/80.skills-simple-bot-to-bot/DialogRootBot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
<version>4.6.0-preview9</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.microsoft.bot</groupId>
<artifactId>bot-builder</artifactId>
<version>4.6.0-preview9</version>
<scope>compile</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
Loading