Merge pull request #205 from pymma/kafka
Allow only users with a user group that gives access to the Workbench…
This commit is contained in:
commit
242da64124
5 changed files with 154 additions and 22 deletions
|
|
@ -101,7 +101,6 @@ public class DababaseContentUpdate {
|
||||||
adminUser.getUserGroups().add(userGroupsRepository.findByName("kiemgmt"));
|
adminUser.getUserGroups().add(userGroupsRepository.findByName("kiemgmt"));
|
||||||
adminUser.getUserGroups().add(userGroupsRepository.findByName("admingroup"));
|
adminUser.getUserGroups().add(userGroupsRepository.findByName("admingroup"));
|
||||||
adminUser.getUserRoles().add(userRolesRepository.findByName("admin"));
|
adminUser.getUserRoles().add(userRolesRepository.findByName("admin"));
|
||||||
adminUser.getUserRoles().add(userRolesRepository.findByName("analyst"));
|
|
||||||
adminUser.getUserRoles().add(userRolesRepository.findByName("rest-all"));
|
adminUser.getUserRoles().add(userRolesRepository.findByName("rest-all"));
|
||||||
userRepository.save(adminUser);
|
userRepository.save(adminUser);
|
||||||
|
|
||||||
|
|
@ -109,7 +108,6 @@ public class DababaseContentUpdate {
|
||||||
nheronUser.getUserGroups().add(userGroupsRepository.findByName("kiemgmt"));
|
nheronUser.getUserGroups().add(userGroupsRepository.findByName("kiemgmt"));
|
||||||
nheronUser.getUserGroups().add(userGroupsRepository.findByName("admingroup"));
|
nheronUser.getUserGroups().add(userGroupsRepository.findByName("admingroup"));
|
||||||
nheronUser.getUserRoles().add(userRolesRepository.findByName("admin"));
|
nheronUser.getUserRoles().add(userRolesRepository.findByName("admin"));
|
||||||
nheronUser.getUserRoles().add(userRolesRepository.findByName("analyst"));
|
|
||||||
nheronUser.getUserRoles().add(userRolesRepository.findByName("rest-all"));
|
nheronUser.getUserRoles().add(userRolesRepository.findByName("rest-all"));
|
||||||
userRepository.save(nheronUser);
|
userRepository.save(nheronUser);
|
||||||
|
|
||||||
|
|
@ -127,6 +125,8 @@ public class DababaseContentUpdate {
|
||||||
String workspaceName = platformProjectData.getSpaceName();
|
String workspaceName = platformProjectData.getSpaceName();
|
||||||
ProjectPersist projectPersist = projectPersistService.saveorUpdateProject(platformProjectData, mainWorkbench);
|
ProjectPersist projectPersist = projectPersistService.saveorUpdateProject(platformProjectData, mainWorkbench);
|
||||||
UserGroups workspaceUserGroups = projectPersistService.createWorkSpaceGroupIfNeeded(workspaceName, mainWorkbench);
|
UserGroups workspaceUserGroups = projectPersistService.createWorkSpaceGroupIfNeeded(workspaceName, mainWorkbench);
|
||||||
|
String result=kieRepositoryService.createSpaceRight(mainWorkbench.getExternalUrl() + "/rest",
|
||||||
|
nheronUser.getLogin(), nheronUser.getPassword(), mainWorkbench.getName(),workspaceUserGroups.getName(),workspaceName);
|
||||||
projectPersistService.createProjectGroupIfNeeded(projectName, mainWorkbench, projectPersist, workspaceUserGroups);
|
projectPersistService.createProjectGroupIfNeeded(projectName, mainWorkbench, projectPersist, workspaceUserGroups);
|
||||||
|
|
||||||
//platformProjectData.getJavaClasses()
|
//platformProjectData.getJavaClasses()
|
||||||
|
|
@ -177,7 +177,7 @@ public class DababaseContentUpdate {
|
||||||
guidedRulesTemplateDefinition = new GuidedRulesTemplateDefinition();
|
guidedRulesTemplateDefinition = new GuidedRulesTemplateDefinition();
|
||||||
guidedRulesTemplateDefinition.setTemplateName(asset.getTitle());
|
guidedRulesTemplateDefinition.setTemplateName(asset.getTitle());
|
||||||
|
|
||||||
guidedRulesTemplateDefinition.setProjectGroup(projectGroupIfNeeded);
|
guidedRulesTemplateDefinition.setProjectGroup(workSpaceGroupIfNeeded);
|
||||||
|
|
||||||
}
|
}
|
||||||
String assetSource = kieRepositoryService.getAssetSource(kieWorkbench.getExternalUrl() + "/rest", nheronUser.getLogin(), nheronUser.getPassword(), workspaceName, projectName, asset.getTitle());
|
String assetSource = kieRepositoryService.getAssetSource(kieWorkbench.getExternalUrl() + "/rest", nheronUser.getLogin(), nheronUser.getPassword(), workspaceName, projectName, asset.getTitle());
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import org.chtijbug.drools.proxy.persistence.repository.UserRepository;
|
||||||
import org.chtijbug.guvnor.server.jaxrs.api.UserLoginInformation;
|
import org.chtijbug.guvnor.server.jaxrs.api.UserLoginInformation;
|
||||||
import org.chtijbug.guvnor.server.jaxrs.jaxb.Asset;
|
import org.chtijbug.guvnor.server.jaxrs.jaxb.Asset;
|
||||||
import org.chtijbug.guvnor.server.jaxrs.model.PlatformProjectData;
|
import org.chtijbug.guvnor.server.jaxrs.model.PlatformProjectData;
|
||||||
|
import org.chtijbug.guvnor.server.jaxrs.model.WorkspaceAuthData;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
@ -271,4 +272,38 @@ public class KieRepositoryService {
|
||||||
HttpHeaders.AUTHORIZATION, authHeader);
|
HttpHeaders.AUTHORIZATION, authHeader);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
public String createSpaceRight(String url, String username, String password,String workbenchName,String groupName,String spaceName) {
|
||||||
|
|
||||||
|
User user = userRepository.findByLogin(username);
|
||||||
|
String completeurl = url + chtijbugprefix+"auth";
|
||||||
|
if (user != null && user.getPassword().equals(password)) {
|
||||||
|
if (user.getCustomer()!= null &&
|
||||||
|
user.getCustomer().getKieWorkbench()!= null
|
||||||
|
&& user.getCustomer().getKieWorkbench().getInternalUrl()!= null){
|
||||||
|
completeurl = user.getCustomer().getKieWorkbench().getInternalUrl()+"/rest/chtijbug/auth";
|
||||||
|
}
|
||||||
|
completeurl=completeurl+"/"+groupName+"/"+spaceName;
|
||||||
|
logger.info("url moteur reco : {}" , completeurl);
|
||||||
|
ResponseEntity<WorkspaceAuthData> response = restTemplateKiewb
|
||||||
|
.execute(completeurl, HttpMethod.POST, requestCallback(null, username, password), clientHttpResponse -> {
|
||||||
|
WorkspaceAuthData extractedResponse =null;
|
||||||
|
if (clientHttpResponse.getBody() != null) {
|
||||||
|
Scanner s = new Scanner(clientHttpResponse.getBody()).useDelimiter("\\A");
|
||||||
|
String result = s.hasNext() ? s.next() : "";
|
||||||
|
extractedResponse = mapper.readValue(result, WorkspaceAuthData.class);
|
||||||
|
|
||||||
|
}
|
||||||
|
return new ResponseEntity<>(extractedResponse, clientHttpResponse.getHeaders(), clientHttpResponse.getStatusCode());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
WorkspaceAuthData responseBody = response.getBody();
|
||||||
|
|
||||||
|
return responseBody.getStatus();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ public class ProjectPersistService {
|
||||||
User groupUser = new User(UUID.randomUUID().toString(), "prj_user_" + projectName, "adminadmin99#");
|
User groupUser = new User(UUID.randomUUID().toString(), "prj_user_" + projectName, "adminadmin99#");
|
||||||
groupUser.getUserGroups().add(projectGroup);
|
groupUser.getUserGroups().add(projectGroup);
|
||||||
groupUser.getUserRoles().add(userRolesRepository.findByName("analyst"));
|
groupUser.getUserRoles().add(userRolesRepository.findByName("analyst"));
|
||||||
|
groupUser.getUserRoles().add(userRolesRepository.findByName("rest-all"));
|
||||||
userRepository.save(groupUser);
|
userRepository.save(groupUser);
|
||||||
} else {
|
} else {
|
||||||
userGroups.setWorkspaceUserGroup(workspaceUserGroup);
|
userGroups.setWorkspaceUserGroup(workspaceUserGroup);
|
||||||
|
|
@ -136,6 +137,7 @@ public class ProjectPersistService {
|
||||||
User groupUser = new User(UUID.randomUUID().toString(), "wrk_user_" + workSpaceName, "pymma#");
|
User groupUser = new User(UUID.randomUUID().toString(), "wrk_user_" + workSpaceName, "pymma#");
|
||||||
groupUser.getUserGroups().add(userGroupsWorkSpace);
|
groupUser.getUserGroups().add(userGroupsWorkSpace);
|
||||||
groupUser.getUserRoles().add(userRolesRepository.findByName("analyst"));
|
groupUser.getUserRoles().add(userRolesRepository.findByName("analyst"));
|
||||||
|
groupUser.getUserRoles().add(userRolesRepository.findByName("rest-all"));
|
||||||
userRepository.save(groupUser);
|
userRepository.save(groupUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.chtijbug.guvnor.server.jaxrs.model;
|
||||||
|
|
||||||
|
public class WorkspaceAuthData {
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import org.chtijbug.guvnor.server.jaxrs.jaxb.Asset;
|
||||||
import org.chtijbug.guvnor.server.jaxrs.jaxb.Package;
|
import org.chtijbug.guvnor.server.jaxrs.jaxb.Package;
|
||||||
import org.chtijbug.guvnor.server.jaxrs.model.DependencyData;
|
import org.chtijbug.guvnor.server.jaxrs.model.DependencyData;
|
||||||
import org.chtijbug.guvnor.server.jaxrs.model.PlatformProjectData;
|
import org.chtijbug.guvnor.server.jaxrs.model.PlatformProjectData;
|
||||||
|
import org.chtijbug.guvnor.server.jaxrs.model.WorkspaceAuthData;
|
||||||
import org.chtijbug.kie.rest.backend.service.AssetService;
|
import org.chtijbug.kie.rest.backend.service.AssetService;
|
||||||
import org.guvnor.common.services.project.model.GAV;
|
import org.guvnor.common.services.project.model.GAV;
|
||||||
import org.guvnor.common.services.project.model.POM;
|
import org.guvnor.common.services.project.model.POM;
|
||||||
|
|
@ -17,18 +18,24 @@ import org.guvnor.structure.organizationalunit.OrganizationalUnitService;
|
||||||
import org.guvnor.structure.repositories.Branch;
|
import org.guvnor.structure.repositories.Branch;
|
||||||
import org.guvnor.structure.repositories.Repository;
|
import org.guvnor.structure.repositories.Repository;
|
||||||
import org.guvnor.structure.repositories.RepositoryService;
|
import org.guvnor.structure.repositories.RepositoryService;
|
||||||
|
import org.jboss.errai.security.shared.api.Group;
|
||||||
|
import org.jboss.errai.security.shared.api.GroupImpl;
|
||||||
import org.kie.workbench.common.screens.datamodeller.service.DataModelerService;
|
import org.kie.workbench.common.screens.datamodeller.service.DataModelerService;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.uberfire.backend.authz.AuthorizationPolicyStorage;
|
||||||
import org.uberfire.backend.authz.AuthorizationService;
|
import org.uberfire.backend.authz.AuthorizationService;
|
||||||
|
import org.uberfire.backend.events.AuthorizationPolicySavedEvent;
|
||||||
import org.uberfire.io.IOService;
|
import org.uberfire.io.IOService;
|
||||||
import org.uberfire.java.nio.base.options.CommentedOption;
|
import org.uberfire.java.nio.base.options.CommentedOption;
|
||||||
import org.uberfire.java.nio.file.DirectoryStream;
|
import org.uberfire.java.nio.file.DirectoryStream;
|
||||||
import org.uberfire.java.nio.file.Paths;
|
import org.uberfire.java.nio.file.Paths;
|
||||||
import org.uberfire.security.authz.AuthorizationPolicy;
|
import org.uberfire.security.authz.AuthorizationPolicy;
|
||||||
|
import org.uberfire.security.authz.Permission;
|
||||||
import org.uberfire.security.authz.PermissionManager;
|
import org.uberfire.security.authz.PermissionManager;
|
||||||
import org.uberfire.security.impl.authz.AuthorizationPolicyBuilder;
|
import org.uberfire.security.impl.authz.AuthorizationPolicyBuilder;
|
||||||
|
|
||||||
import javax.enterprise.context.ApplicationScoped;
|
import javax.enterprise.context.ApplicationScoped;
|
||||||
|
import javax.enterprise.event.Event;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
|
|
@ -37,6 +44,7 @@ import java.io.File;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.security.Principal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Path("/chtijbug")
|
@Path("/chtijbug")
|
||||||
|
|
@ -76,6 +84,11 @@ public class PackageResource {
|
||||||
@Inject
|
@Inject
|
||||||
private UserManagementResourceHelper userManagementResourceHelper;
|
private UserManagementResourceHelper userManagementResourceHelper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private AuthorizationPolicyStorage authorizationPolicyStorage;
|
||||||
|
@Inject
|
||||||
|
private Event<AuthorizationPolicySavedEvent> savedEvent;
|
||||||
|
|
||||||
public PackageResource() {
|
public PackageResource() {
|
||||||
System.out.println("coucou");
|
System.out.println("coucou");
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +109,7 @@ public class PackageResource {
|
||||||
return userLoginInformation;
|
return userLoginInformation;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/content")
|
@Path("/content")
|
||||||
|
|
@ -113,6 +127,7 @@ public class PackageResource {
|
||||||
return userLoginInformation;
|
return userLoginInformation;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/detailedSpaces")
|
@Path("/detailedSpaces")
|
||||||
|
|
@ -150,7 +165,6 @@ public class PackageResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{organizationalUnitName}/{projectName}/assets")
|
@Path("{organizationalUnitName}/{projectName}/assets")
|
||||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
|
||||||
|
|
@ -466,21 +480,89 @@ public class PackageResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/auth")
|
@Path("/auth")
|
||||||
public AuthorizationPolicy getAuth() {
|
public AuthorizationPolicy getAuth() {
|
||||||
|
Principal Principam = sc.getUserPrincipal();
|
||||||
AuthorizationPolicy authorizationPolicy = this.permissionManager.getAuthorizationPolicy();
|
AuthorizationPolicy authorizationPolicy = this.permissionManager.getAuthorizationPolicy();
|
||||||
return authorizationPolicy;
|
return authorizationPolicy;
|
||||||
}
|
}
|
||||||
@GET
|
|
||||||
|
@POST
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/auth2")
|
@Path("/auth/{groupName}/{organisationUnit}")
|
||||||
public AuthorizationPolicy getAuth2() {
|
public Response createGroupAuthorization(@Context HttpHeaders headers,
|
||||||
AuthorizationPolicyBuilder tata = this.permissionManager.newAuthorizationPolicy();
|
@PathParam("groupName") String groupName,
|
||||||
|
@PathParam("organisationUnit") String organisationUnit) {
|
||||||
|
|
||||||
AuthorizationPolicy authorizationPolicy = this.permissionManager.getAuthorizationPolicy();
|
Group targetGroup = null;
|
||||||
return authorizationPolicy;
|
AuthorizationPolicy storedPolicies = this.authorizationPolicyStorage.loadPolicy();
|
||||||
|
for (Group group : storedPolicies.getGroups()) {
|
||||||
|
if (group.getName().equals(groupName)) {
|
||||||
|
targetGroup = group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetGroup == null) {
|
||||||
|
targetGroup = new GroupImpl(groupName);
|
||||||
|
AuthorizationPolicyBuilder groupPermissionBuilder = permissionManager.newAuthorizationPolicy().group(groupName);
|
||||||
|
|
||||||
|
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("editor.read", true);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission("dataobject.edit", true);
|
||||||
|
//groupPermissionBuilder = groupPermissionBuilder.permission("editor.read.BPMNDiagramEditor", true);
|
||||||
|
//groupPermissionBuilder = groupPermissionBuilder.permission("editor.read.CaseManagementDiagramEditor", true);
|
||||||
|
//groupPermissionBuilder = groupPermissionBuilder.permission("editor.read.GuidedDecisionTreeEditorPresenter", true);
|
||||||
|
//groupPermissionBuilder = groupPermissionBuilder.permission("editor.read.GuidedScoreCardEditor", true);
|
||||||
|
//groupPermissionBuilder = groupPermissionBuilder.permission("editor.read.ScoreCardXLSEditor", true);
|
||||||
|
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("globalExperimentalFeatures.edit", true);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "globalpreferences.edit", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("guideddecisiontable.edit.columns", true);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("jar.download", true);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "orgunit.create", false);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "orgunit.delete", false);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "orgunit.read", false);
|
||||||
|
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("orgunit.read." + organisationUnit, true);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "orgunit.update", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("orgunit.update." + organisationUnit, true);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "perspective.create", false);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "perspective.delete", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("perspective.read", true);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "perspective.update", false);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "planner.available", false);
|
||||||
|
// groupPermissionBuilder = groupPermissionBuilder.permission(groupPermissionbase + "profilepreferences.edit", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("project.build", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("project.create", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("project.delete", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("project.read", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("project.release", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("project.update", false);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("repository.build", true);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("repository.configure", true);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("repository.create", true);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("repository.delete", true);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("repository.read", true);
|
||||||
|
groupPermissionBuilder = groupPermissionBuilder.permission("repository.update", true);
|
||||||
|
//groupPermissionBuilder = groupPermissionBuilder.priority(-10);
|
||||||
|
|
||||||
|
for (Permission p : groupPermissionBuilder.build().getPermissions(targetGroup).collection()) {
|
||||||
|
storedPolicies.addPermission(targetGroup, p);
|
||||||
|
}
|
||||||
|
storedPolicies.setHomePerspective(targetGroup,"AuthoringPerspective");
|
||||||
|
storedPolicies.setPriority(targetGroup,-10);
|
||||||
|
this.authorizationPolicyStorage.savePolicy(storedPolicies);
|
||||||
|
permissionManager.setAuthorizationPolicy(storedPolicies);
|
||||||
|
savedEvent.fire(new AuthorizationPolicySavedEvent(storedPolicies));
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
WorkspaceAuthData result=new WorkspaceAuthData();
|
||||||
|
return Response.status(Response.Status.OK).entity(result).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
editor.link_modal.header
Reference in a new issue