com.composum.ai.backend.slingbase.impl.AIConfigurationServiceImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of composum-ai-integration-backend-slingbase Show documentation
Show all versions of composum-ai-integration-backend-slingbase Show documentation
Common Functionality for Composum AI specific to Sling but would be useable in both Composum and AEM and similar.
package com.composum.ai.backend.slingbase.impl;
import static com.composum.ai.backend.slingbase.impl.AllowDenyMatcherUtil.matchesAny;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.jcr.RepositoryException;
import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.composum.ai.backend.base.service.chat.GPTChatCompletionService;
import com.composum.ai.backend.base.service.chat.GPTConfiguration;
import com.composum.ai.backend.slingbase.AIConfigurationPlugin;
import com.composum.ai.backend.slingbase.AIConfigurationService;
import com.composum.ai.backend.slingbase.model.GPTPermissionConfiguration;
import com.composum.ai.backend.slingbase.model.GPTPermissionInfo;
import com.composum.ai.backend.slingbase.model.GPTPromptLibrary;
/**
* Collects the configurations from {@link AIConfigurationPlugin}s and aggregates them.
*
*
* The primary responsibility of this class is to determine which AI services are allowed based on various parameters such as:
*
* - The user or user group making the request.
* - The content path being accessed or edited.
* - The URL of the editor in the browser.
*
*
*
*
* The configurations are defined as OSGI configurations and can be dynamically modified at runtime. Each configuration specifies:
*
* - Allowed and denied users or user groups.
* - Allowed and denied content paths.
* - Allowed and denied views (based on the URL).
* - The specific AI services that the configuration applies to.
*
*
*
*
* When determining the allowed services, this implementation checks all the available configurations and aggregates the results.
* A service is considered allowed if it matches any of the "allowed" regular expressions and does not match any of the "denied" regular expressions.
*
*
* @see AIConfigurationPlugin
* @see GPTPermissionConfiguration
*/
@Component(service = AIConfigurationService.class)
public class AIConfigurationServiceImpl implements AIConfigurationService {
private static final Logger LOG = LoggerFactory.getLogger(AIConfigurationServiceImpl.class);
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
protected volatile List plugins;
@Reference
protected GPTChatCompletionService chatCompletionService;
/**
* Union of the plugin's results.
*/
@Override
@Nullable
public GPTPermissionInfo allowedServices(@Nonnull SlingHttpServletRequest request, @Nonnull String contentPath, @Nonnull String editorUrl) {
if (contentPath == null || !contentPath.startsWith("/content/")) {
return null;
}
if (request.getResourceResolver().getResource(contentPath) == null) {
// During the creation of a new component it's possible that there are new subcomponents that haven't been materialized yet.
// Example: in Composum when a row is created, the columns are only created when a subcomponent of the column is saved.
if (request.getResourceResolver().getResource(ResourceUtil.getParent(contentPath)) != null &&
!contentPath.endsWith(JcrConstants.JCR_CONTENT)) {
contentPath = ResourceUtil.getParent(contentPath);
}
}
GPTConfiguration gptConfiguration = getGPTConfiguration(request.getResourceResolver(), contentPath);
GPTPermissionInfo result = null;
if (chatCompletionService.isEnabled(gptConfiguration)) {
for (AIConfigurationPlugin plugin : plugins) {
try {
List configs = plugin.allowedServices(request, contentPath);
if (configs != null) {
for (GPTPermissionConfiguration config : configs) {
if (basicCheck(config, request, contentPath, editorUrl)) {
GPTPermissionInfo permissionInfo = GPTPermissionInfo.from(config);
result = GPTPermissionInfo.mergeAdditively(result, permissionInfo);
if (configs != null) {
LOG.info("Plugin {} allowed services {}", plugin.getClass(), permissionInfo);
}
}
}
}
} catch (Exception e) {
LOG.error("Error in AIConfigurationPlugin with {}", plugin.getClass(), e);
}
}
}
return result;
}
/**
* Determines whether the configuration allows access wrt. user, page and view
*/
protected boolean basicCheck(@Nonnull GPTPermissionConfiguration config, SlingHttpServletRequest request, @Nonnull String contentPath, @Nonnull String editorUrl) {
boolean allowed = false;
try {
List userAndGroups = AllowDenyMatcherUtil.userAndGroupsOfUser(request);
// A user is allowed if his username or any of the groups he is in matches the allowedUsers regexes and
// none of them matches the deniedUsers regexes.
boolean userAllowed = false;
boolean userDenied = false;
for (String userOrGroup : userAndGroups) {
userAllowed = userAllowed || matchesAny(userOrGroup, config.allowedUsers());
userDenied = userDenied || matchesAny(userOrGroup, config.deniedUsers());
}
boolean pathAllowed = matchesAny(contentPath, config.allowedPaths()) && !matchesAny(contentPath, config.deniedPaths());
boolean viewAllowed = matchesAny(editorUrl, config.allowedViews()) && !matchesAny(editorUrl, config.deniedViews());
allowed = userAllowed && !userDenied && pathAllowed && viewAllowed;
allowed = allowed && pageAllowed(request, contentPath, config);
} catch (RepositoryException | RuntimeException e) {
LOG.error("Error determining allowed services for {} {} {}", request.getRemoteUser(), contentPath, editorUrl, e);
}
return allowed;
}
protected boolean pageAllowed(SlingHttpServletRequest request, String contentPath, GPTPermissionConfiguration config) {
Resource resource = request.getResourceResolver().getResource(contentPath);
if (resource == null) {
LOG.warn("Resource {} not found", contentPath);
return false;
}
// go to next transitive parent jcr:content node - the page containing the component
Resource page = resource;
if (page.getChild(JcrConstants.JCR_CONTENT) != null) {
page = page.getChild(JcrConstants.JCR_CONTENT);
}
while (page != null && !JcrConstants.JCR_CONTENT.equals(page.getName())) {
page = page.getParent();
}
if (page == null) {
LOG.warn("No page found for resource {}", resource.getPath());
return false;
}
ValueMap valueMap = page.getValueMap();
String template = valueMap.get("cq:template", String.class); // AEM
if (template == null) {
template = valueMap.get("template", String.class); // Composum
}
if (template == null) { // for content fragments we use the cq:model
template = valueMap.get("data/cq:model", String.class);
}
return matchesAny(template, config.allowedPageTemplates()) && !matchesAny(template, config.deniedPageTemplates());
}
@Override
public GPTConfiguration getGPTConfiguration(@NotNull ResourceResolver resourceResolver, @Nullable String contentPath) throws IllegalArgumentException {
for (AIConfigurationPlugin plugin : plugins) {
try {
GPTConfiguration configuration = plugin.getGPTConfiguration(resourceResolver, contentPath);
if (configuration != null) {
LOG.info("Plugin {} returned configuration {}", plugin.getClass(), configuration);
return configuration;
}
} catch (Exception e) {
LOG.error("Error in AIConfigurationPlugin with {}", plugin.getClass(), e);
}
}
return null;
}
@Nullable
@Override
public GPTPromptLibrary getGPTPromptLibraryPaths(@NotNull SlingHttpServletRequest request, @Nullable String contentPath) throws IllegalArgumentException {
return new GPTPromptLibrary() {
@Override
public String contentCreationPromptsPath() {
Optional result = plugins.stream()
.map(plugin -> plugin.getGPTPromptLibraryPaths(request, contentPath))
.filter(Objects::nonNull)
.map(GPTPromptLibrary::contentCreationPromptsPath)
.filter(Objects::nonNull)
.findFirst();
if (result.isPresent()) {
return result.get();
}
return plugins.stream()
.map(plugin -> plugin.getGPTPromptLibraryPathsDefault())
.filter(Objects::nonNull)
.map(GPTPromptLibrary::contentCreationPromptsPath)
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
@Override
public String sidePanelPromptsPath() {
Optional result = plugins.stream()
.map(plugin -> plugin.getGPTPromptLibraryPaths(request, contentPath))
.filter(Objects::nonNull)
.map(GPTPromptLibrary::sidePanelPromptsPath)
.filter(Objects::nonNull)
.findFirst();
if (result.isPresent()) {
return result.get();
}
return plugins.stream()
.map(plugin -> plugin.getGPTPromptLibraryPathsDefault())
.filter(Objects::nonNull)
.map(GPTPromptLibrary::sidePanelPromptsPath)
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
@Override
public Class extends Annotation> annotationType() {
return GPTPromptLibrary.class;
}
};
}
@Nullable
@Override
public Map getGPTConfigurationMap(@NotNull SlingHttpServletRequest request, @Nullable String mapPath, @Nullable String languageCode) {
return plugins.stream()
.map(plugin -> plugin.getGPTConfigurationMap(request, mapPath, languageCode))
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
}