Merge branch 'release'
This commit is contained in:
commit
71298cd3f8
10
.github/workflows/server.yml
vendored
10
.github/workflows/server.yml
vendored
|
|
@ -68,15 +68,15 @@ jobs:
|
||||||
- name: Push release image to Docker Hub
|
- name: Push release image to Docker Hub
|
||||||
if: success() && github.ref == 'refs/heads/release'
|
if: success() && github.ref == 'refs/heads/release'
|
||||||
run: |
|
run: |
|
||||||
docker build -t appsmith/appsmith-server-ee:${{steps.vars.outputs.tag}} .
|
docker build -t appsmith/appsmith-server:${{steps.vars.outputs.tag}} .
|
||||||
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
docker push appsmith/appsmith-server-ee
|
docker push appsmith/appsmith-server
|
||||||
|
|
||||||
# Build master Docker image and push to Docker Hub
|
# Build master Docker image and push to Docker Hub
|
||||||
- name: Push master image to Docker Hub with commit tag
|
- name: Push master image to Docker Hub with commit tag
|
||||||
if: success() && github.ref == 'refs/heads/master'
|
if: success() && github.ref == 'refs/heads/master'
|
||||||
run: |
|
run: |
|
||||||
docker build -t appsmith/appsmith-server-ee:${GITHUB_SHA} .
|
docker build -t appsmith/appsmith-server:${GITHUB_SHA} .
|
||||||
docker build -t appsmith/appsmith-server-ee:latest .
|
docker build -t appsmith/appsmith-server:latest .
|
||||||
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
docker push appsmith/appsmith-server-ee
|
docker push appsmith/appsmith-server
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,12 @@ This will
|
||||||
$ cd ./dist
|
$ cd ./dist
|
||||||
$ java -jar -Dspring.profiles.active=$env server-1.0-SNAPSHOT.jar
|
$ java -jar -Dspring.profiles.active=$env server-1.0-SNAPSHOT.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
In order to test the code, you can run the following command
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
Please make sure that you have a local Redis instance running for the test cases. The MongoDB is run in-memory during tests so that shouldn't be a problem.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package com.appsmith.server.configurations;
|
||||||
import com.appsmith.server.authentication.handlers.AccessDeniedHandler;
|
import com.appsmith.server.authentication.handlers.AccessDeniedHandler;
|
||||||
import com.appsmith.server.authentication.handlers.CustomServerOAuth2AuthorizationRequestResolver;
|
import com.appsmith.server.authentication.handlers.CustomServerOAuth2AuthorizationRequestResolver;
|
||||||
import com.appsmith.server.authentication.handlers.LogoutSuccessHandler;
|
import com.appsmith.server.authentication.handlers.LogoutSuccessHandler;
|
||||||
|
import com.appsmith.server.constants.FieldName;
|
||||||
import com.appsmith.server.constants.Url;
|
import com.appsmith.server.constants.Url;
|
||||||
import com.appsmith.server.domains.User;
|
import com.appsmith.server.domains.User;
|
||||||
import com.appsmith.server.services.UserService;
|
import com.appsmith.server.services.UserService;
|
||||||
|
|
@ -161,8 +162,8 @@ public class SecurityConfig {
|
||||||
|
|
||||||
private User createAnonymousUser() {
|
private User createAnonymousUser() {
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setName("anonymousUser");
|
user.setName(FieldName.ANONYMOUS_USER);
|
||||||
user.setEmail("anonymousUser");
|
user.setEmail(FieldName.ANONYMOUS_USER);
|
||||||
user.setCurrentOrganizationId("");
|
user.setCurrentOrganizationId("");
|
||||||
user.setOrganizationIds(new HashSet<>());
|
user.setOrganizationIds(new HashSet<>());
|
||||||
user.setIsAnonymous(true);
|
user.setIsAnonymous(true);
|
||||||
|
|
|
||||||
|
|
@ -48,4 +48,5 @@ public class FieldName {
|
||||||
" \"leftColumn\": 0,\n" +
|
" \"leftColumn\": 0,\n" +
|
||||||
" \"children\": []\n" +
|
" \"children\": []\n" +
|
||||||
"}";
|
"}";
|
||||||
|
public static String ANONYMOUS_USER = "anonymousUser";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,16 @@ public abstract class BaseAppsmithRepositoryImpl<T extends BaseDomain> {
|
||||||
.and("permission").is(permission.getValue())
|
.and("permission").is(permission.getValue())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Criteria anonymousUserCriteria = Criteria.where(fieldName(QBaseDomain.baseDomain.policies))
|
||||||
|
.elemMatch(Criteria.where("users").all(FieldName.ANONYMOUS_USER)
|
||||||
|
.and("permission").is(permission.getValue())
|
||||||
|
);
|
||||||
|
|
||||||
Criteria groupCriteria = Criteria.where(fieldName(QBaseDomain.baseDomain.policies))
|
Criteria groupCriteria = Criteria.where(fieldName(QBaseDomain.baseDomain.policies))
|
||||||
.elemMatch(Criteria.where("groups").all(user.getGroupIds())
|
.elemMatch(Criteria.where("groups").all(user.getGroupIds())
|
||||||
.and("permission").is(permission.getValue()));
|
.and("permission").is(permission.getValue()));
|
||||||
|
|
||||||
return new Criteria().orOperator(userCriteria, groupCriteria);
|
return new Criteria().orOperator(userCriteria, groupCriteria, anonymousUserCriteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Criteria getIdCriteria(Object id) {
|
protected Criteria getIdCriteria(Object id) {
|
||||||
|
|
@ -187,7 +192,8 @@ public abstract class BaseAppsmithRepositoryImpl<T extends BaseDomain> {
|
||||||
Set<String> policyGroups = policy.getGroups();
|
Set<String> policyGroups = policy.getGroups();
|
||||||
|
|
||||||
|
|
||||||
if (policyUsers != null && policyUsers.contains(user.getUsername())) {
|
if (policyUsers != null &&
|
||||||
|
(policyUsers.contains(user.getUsername()) || policyUsers.contains(FieldName.ANONYMOUS_USER))) {
|
||||||
permissions.add(policy.getPermission());
|
permissions.add(policy.getPermission());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
package com.appsmith.server.services;
|
package com.appsmith.server.services;
|
||||||
|
|
||||||
|
import com.appsmith.external.models.Policy;
|
||||||
import com.appsmith.server.acl.AclPermission;
|
import com.appsmith.server.acl.AclPermission;
|
||||||
import com.appsmith.server.constants.AnalyticsEvents;
|
import com.appsmith.server.constants.AnalyticsEvents;
|
||||||
import com.appsmith.server.constants.FieldName;
|
import com.appsmith.server.constants.FieldName;
|
||||||
|
import com.appsmith.server.domains.Action;
|
||||||
import com.appsmith.server.domains.Application;
|
import com.appsmith.server.domains.Application;
|
||||||
import com.appsmith.server.domains.ApplicationPage;
|
import com.appsmith.server.domains.ApplicationPage;
|
||||||
import com.appsmith.server.domains.Layout;
|
import com.appsmith.server.domains.Layout;
|
||||||
import com.appsmith.server.domains.Organization;
|
import com.appsmith.server.domains.Organization;
|
||||||
|
import com.appsmith.server.domains.Page;
|
||||||
import com.appsmith.server.domains.User;
|
import com.appsmith.server.domains.User;
|
||||||
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
||||||
import com.appsmith.server.dtos.OrganizationApplicationsDTO;
|
import com.appsmith.server.dtos.OrganizationApplicationsDTO;
|
||||||
import com.appsmith.server.dtos.UserHomepageDTO;
|
import com.appsmith.server.dtos.UserHomepageDTO;
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
import com.appsmith.server.exceptions.AppsmithException;
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
|
import com.appsmith.server.helpers.PolicyUtils;
|
||||||
import com.appsmith.server.repositories.ApplicationRepository;
|
import com.appsmith.server.repositories.ApplicationRepository;
|
||||||
import com.appsmith.server.repositories.PageRepository;
|
import com.appsmith.server.repositories.PageRepository;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -26,7 +30,6 @@ import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Scheduler;
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
import java.text.Format;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
|
@ -51,6 +54,7 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
|
||||||
private final SessionUserService sessionUserService;
|
private final SessionUserService sessionUserService;
|
||||||
private final OrganizationService organizationService;
|
private final OrganizationService organizationService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final PolicyUtils policyUtils;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ApplicationServiceImpl(Scheduler scheduler,
|
public ApplicationServiceImpl(Scheduler scheduler,
|
||||||
|
|
@ -62,12 +66,14 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
|
||||||
PageRepository pageRepository,
|
PageRepository pageRepository,
|
||||||
SessionUserService sessionUserService,
|
SessionUserService sessionUserService,
|
||||||
OrganizationService organizationService,
|
OrganizationService organizationService,
|
||||||
UserService userService) {
|
UserService userService,
|
||||||
|
PolicyUtils policyUtils) {
|
||||||
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
|
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
|
||||||
this.pageRepository = pageRepository;
|
this.pageRepository = pageRepository;
|
||||||
this.sessionUserService = sessionUserService;
|
this.sessionUserService = sessionUserService;
|
||||||
this.organizationService = organizationService;
|
this.organizationService = organizationService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.policyUtils = policyUtils;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -241,8 +247,52 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
|
||||||
.findById(id, MANAGE_APPLICATIONS)
|
.findById(id, MANAGE_APPLICATIONS)
|
||||||
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION_ID, id)))
|
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION_ID, id)))
|
||||||
.flatMap(application -> {
|
.flatMap(application -> {
|
||||||
|
|
||||||
|
if (application.getIsPublic().equals(applicationAccessDTO.getPublicAccess())) {
|
||||||
|
// No change. The required public access is the same as current public access. Do nothing
|
||||||
|
return Mono.just(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (application.getIsPublic() == null && applicationAccessDTO.getPublicAccess().equals(false)) {
|
||||||
|
return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION));
|
||||||
|
}
|
||||||
|
|
||||||
application.setIsPublic(applicationAccessDTO.getPublicAccess());
|
application.setIsPublic(applicationAccessDTO.getPublicAccess());
|
||||||
return repository.save(application);
|
return generateAndSetPoliciesForPublicView(application, applicationAccessDTO.getPublicAccess());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<Application> generateAndSetPoliciesForPublicView(Application application, Boolean isPublic) {
|
||||||
|
AclPermission permission = READ_APPLICATIONS;
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setName(FieldName.ANONYMOUS_USER);
|
||||||
|
user.setEmail(FieldName.ANONYMOUS_USER);
|
||||||
|
user.setIsAnonymous(true);
|
||||||
|
|
||||||
|
Map<String, Policy> applicationPolicyMap = policyUtils.generatePolicyFromPermission(Set.of(permission), user);
|
||||||
|
Map<String, Policy> pagePolicyMap = policyUtils.generatePagePoliciesFromApplicationPolicies(applicationPolicyMap, user);
|
||||||
|
Map<String, Policy> actionPolicyMap = policyUtils.generateActionPoliciesFromPagePolicies(pagePolicyMap, user);
|
||||||
|
|
||||||
|
Flux<Page> updatedPagesFlux = policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, isPublic);
|
||||||
|
|
||||||
|
Flux<Action> updatedActionsFlux = updatedPagesFlux
|
||||||
|
.flatMap(page -> policyUtils.updateWithPagePermissionsToAllItsActions(page.getId(), actionPolicyMap, isPublic));
|
||||||
|
|
||||||
|
return updatedActionsFlux
|
||||||
|
.collectList()
|
||||||
|
.thenReturn(application)
|
||||||
|
.flatMap(app -> {
|
||||||
|
Application updatedApplication;
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
updatedApplication = (Application) policyUtils.addPoliciesToExistingObject(applicationPolicyMap, (Application) application);
|
||||||
|
} else {
|
||||||
|
updatedApplication = (Application) policyUtils.removePoliciesFromExistingObject(applicationPolicyMap, (Application) application);
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.save(updatedApplication);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,10 +6,12 @@ import com.appsmith.server.domains.Application;
|
||||||
import com.appsmith.server.domains.Organization;
|
import com.appsmith.server.domains.Organization;
|
||||||
import com.appsmith.server.domains.Page;
|
import com.appsmith.server.domains.Page;
|
||||||
import com.appsmith.server.domains.User;
|
import com.appsmith.server.domains.User;
|
||||||
|
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
||||||
import com.appsmith.server.dtos.OrganizationApplicationsDTO;
|
import com.appsmith.server.dtos.OrganizationApplicationsDTO;
|
||||||
import com.appsmith.server.dtos.UserHomepageDTO;
|
import com.appsmith.server.dtos.UserHomepageDTO;
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
import com.appsmith.server.exceptions.AppsmithException;
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
|
import com.appsmith.server.repositories.PageRepository;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -55,6 +57,9 @@ public class ApplicationServiceTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
OrganizationService organizationService;
|
OrganizationService organizationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
PageRepository pageRepository;
|
||||||
|
|
||||||
String orgId;
|
String orgId;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
@ -206,11 +211,14 @@ public class ApplicationServiceTest {
|
||||||
.block();
|
.block();
|
||||||
|
|
||||||
assertThat(applicationList.size() > 0);
|
assertThat(applicationList.size() > 0);
|
||||||
applicationList.forEach(t -> {
|
applicationList
|
||||||
assertThat(t.getId()).isNotNull();
|
.stream()
|
||||||
assertThat(t.getPolicies()).isNotEmpty();
|
.filter(t -> t.getName().equals("validGetApplications-Test"))
|
||||||
assertThat(t.getPolicies()).containsAll(Set.of(readAppPolicy));
|
.forEach(t -> {
|
||||||
});
|
assertThat(t.getId()).isNotNull();
|
||||||
|
assertThat(t.getPolicies()).isNotEmpty();
|
||||||
|
assertThat(t.getPolicies()).containsAll(Set.of(readAppPolicy));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tests for Update Application Flow */
|
/* Tests for Update Application Flow */
|
||||||
|
|
@ -322,4 +330,109 @@ public class ApplicationServiceTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails(value = "api_user")
|
||||||
|
public void validMakeApplicationPublic() {
|
||||||
|
Application application = new Application();
|
||||||
|
application.setName("validMakeApplicationPublic-Test");
|
||||||
|
|
||||||
|
Policy manageAppPolicy = Policy.builder().permission(MANAGE_APPLICATIONS.getValue())
|
||||||
|
.users(Set.of("api_user"))
|
||||||
|
.build();
|
||||||
|
Policy readAppPolicy = Policy.builder().permission(READ_APPLICATIONS.getValue())
|
||||||
|
.users(Set.of("api_user", FieldName.ANONYMOUS_USER))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue())
|
||||||
|
.users(Set.of("api_user"))
|
||||||
|
.build();
|
||||||
|
Policy readPagePolicy = Policy.builder().permission(READ_PAGES.getValue())
|
||||||
|
.users(Set.of("api_user", FieldName.ANONYMOUS_USER))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Application createdApplication = applicationPageService.createApplication(application, orgId).block();
|
||||||
|
|
||||||
|
ApplicationAccessDTO applicationAccessDTO = new ApplicationAccessDTO();
|
||||||
|
applicationAccessDTO.setPublicAccess(true);
|
||||||
|
|
||||||
|
Mono<Application> publicAppMono = applicationService
|
||||||
|
.changeViewAccess(createdApplication.getId(), applicationAccessDTO)
|
||||||
|
.cache();
|
||||||
|
|
||||||
|
Mono<Page> pageMono = publicAppMono
|
||||||
|
.flatMap(app -> {
|
||||||
|
String pageId = app.getPages().get(0).getId();
|
||||||
|
return pageRepository.findById(pageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
StepVerifier
|
||||||
|
.create(Mono.zip(publicAppMono, pageMono))
|
||||||
|
.assertNext(tuple -> {
|
||||||
|
Application publicApp = tuple.getT1();
|
||||||
|
Page page = tuple.getT2();
|
||||||
|
|
||||||
|
assertThat(publicApp.getIsPublic()).isTrue();
|
||||||
|
assertThat(publicApp.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy));
|
||||||
|
|
||||||
|
// Check the child page's policies
|
||||||
|
assertThat(page.getPolicies()).containsAll(Set.of(managePagePolicy, readPagePolicy));
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails(value = "api_user")
|
||||||
|
public void validMakeApplicationPrivate() {
|
||||||
|
Application application = new Application();
|
||||||
|
application.setName("validMakeApplicationPrivate-Test");
|
||||||
|
|
||||||
|
Policy manageAppPolicy = Policy.builder().permission(MANAGE_APPLICATIONS.getValue())
|
||||||
|
.users(Set.of("api_user"))
|
||||||
|
.build();
|
||||||
|
Policy readAppPolicy = Policy.builder().permission(READ_APPLICATIONS.getValue())
|
||||||
|
.users(Set.of("api_user"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue())
|
||||||
|
.users(Set.of("api_user"))
|
||||||
|
.build();
|
||||||
|
Policy readPagePolicy = Policy.builder().permission(READ_PAGES.getValue())
|
||||||
|
.users(Set.of("api_user"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Mono<Application> createApplication = applicationPageService.createApplication(application, orgId);
|
||||||
|
|
||||||
|
ApplicationAccessDTO applicationAccessDTO = new ApplicationAccessDTO();
|
||||||
|
Mono<Application> privateAppMono = createApplication
|
||||||
|
.flatMap(application1 -> {
|
||||||
|
applicationAccessDTO.setPublicAccess(true);
|
||||||
|
return applicationService.changeViewAccess(application1.getId(), applicationAccessDTO);
|
||||||
|
})
|
||||||
|
.flatMap(application1 -> {
|
||||||
|
applicationAccessDTO.setPublicAccess(false);
|
||||||
|
return applicationService.changeViewAccess(application1.getId(), applicationAccessDTO);
|
||||||
|
})
|
||||||
|
.cache();
|
||||||
|
|
||||||
|
Mono<Page> pageMono = privateAppMono
|
||||||
|
.flatMap(app -> {
|
||||||
|
String pageId = app.getPages().get(0).getId();
|
||||||
|
return pageRepository.findById(pageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
StepVerifier
|
||||||
|
.create(Mono.zip(privateAppMono, pageMono))
|
||||||
|
.assertNext(tuple -> {
|
||||||
|
Application app = tuple.getT1();
|
||||||
|
Page page = tuple.getT2();
|
||||||
|
|
||||||
|
assertThat(app.getIsPublic()).isFalse();
|
||||||
|
assertThat(app.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy));
|
||||||
|
|
||||||
|
// Check the child page's policies
|
||||||
|
assertThat(page.getPolicies()).containsAll(Set.of(managePagePolicy, readPagePolicy));
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user