Merge pull request #3766 from pedroigor/KEYCLOAK-4203
[KEYCLOAK-4203] - Removing references to Drools
This commit is contained in:
commit
c7f2a0ffdd
23 changed files with 5645 additions and 2775 deletions
|
@ -35,7 +35,7 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
|
|||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Rule";
|
||||
return "Rules";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,7 +75,7 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
|
|||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "drools";
|
||||
return "rules";
|
||||
}
|
||||
|
||||
void update(Policy policy) {
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
{
|
||||
"name": "Only Owner Policy",
|
||||
"description": "Defines that only the resource owner is allowed to do something",
|
||||
"type": "drools",
|
||||
"type": "rules",
|
||||
"logic": "POSITIVE",
|
||||
"decisionStrategy": "UNANIMOUS",
|
||||
"config": {
|
||||
|
|
29
model/jpa/src/main/resources/META-INF/jpa-changelog-authz-2.5.1.xml
Executable file
29
model/jpa/src/main/resources/META-INF/jpa-changelog-authz-2.5.1.xml
Executable file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
~ * Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ * and other contributors as indicated by the @author tags.
|
||||
~ *
|
||||
~ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ * you may not use this file except in compliance with the License.
|
||||
~ * You may obtain a copy of the License at
|
||||
~ *
|
||||
~ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
~ *
|
||||
~ * Unless required by applicable law or agreed to in writing, software
|
||||
~ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ * See the License for the specific language governing permissions and
|
||||
~ * limitations under the License.
|
||||
-->
|
||||
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
|
||||
<changeSet author="psilva@redhat.com" id="authz-2.5.1">
|
||||
<update tableName="RESOURCE_SERVER_POLICY">
|
||||
<column name="TYPE" value="rules"/>
|
||||
<where>TYPE = :value</where>
|
||||
<whereParams>
|
||||
<param value="drools" />
|
||||
</whereParams>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
|
@ -19,4 +19,5 @@
|
|||
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
|
||||
<include file="META-INF/jpa-changelog-authz-2.0.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-authz-2.5.1.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -56,7 +56,7 @@ public class MigrationModelManager {
|
|||
new MigrateTo2_1_0(),
|
||||
new MigrateTo2_2_0(),
|
||||
new MigrateTo2_3_0(),
|
||||
new MigrateTo2_5_0(),
|
||||
new MigrateTo2_5_0()
|
||||
};
|
||||
|
||||
public static void migrate(KeycloakSession session) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.authorization.model.Policy;
|
|||
import org.keycloak.authorization.model.Resource;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
import org.keycloak.authorization.store.PolicyStore;
|
||||
import org.keycloak.authorization.store.ResourceServerStore;
|
||||
import org.keycloak.authorization.store.ResourceStore;
|
||||
|
@ -2055,6 +2056,19 @@ public class RepresentationToModel {
|
|||
}
|
||||
|
||||
public static Policy toModel(PolicyRepresentation policy, ResourceServer resourceServer, AuthorizationProvider authorization) {
|
||||
String type = policy.getType();
|
||||
PolicyProvider provider = authorization.getProvider(type);
|
||||
|
||||
if (provider == null) {
|
||||
//TODO: temporary, remove this check on future versions as drools type is now deprecated
|
||||
if ("drools".equalsIgnoreCase(type)) {
|
||||
type = "rules";
|
||||
}
|
||||
if (authorization.getProvider(type) == null) {
|
||||
throw new RuntimeException("Unknown polucy type [" + type + "]. Could not find a provider for this type.");
|
||||
}
|
||||
}
|
||||
|
||||
PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore();
|
||||
Policy existing;
|
||||
|
||||
|
@ -2078,7 +2092,7 @@ public class RepresentationToModel {
|
|||
return existing;
|
||||
}
|
||||
|
||||
Policy model = policyStore.create(policy.getName(), policy.getType(), resourceServer);
|
||||
Policy model = policyStore.create(policy.getName(), type, resourceServer);
|
||||
|
||||
model.setDescription(policy.getDescription());
|
||||
model.setDecisionStrategy(policy.getDecisionStrategy());
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
{
|
||||
"name": "Only Owner Policy",
|
||||
"description": "Defines that only the resource owner is allowed to do something",
|
||||
"type": "drools",
|
||||
"type": "rules",
|
||||
"logic": "POSITIVE",
|
||||
"decisionStrategy": "UNANIMOUS",
|
||||
"config": {
|
||||
|
|
|
@ -84,6 +84,18 @@
|
|||
</dependencies>
|
||||
|
||||
<build>
|
||||
<testResources>
|
||||
<testResource>
|
||||
<directory>src/test/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>migration-test/*</include>
|
||||
</includes>
|
||||
</testResource>
|
||||
<testResource>
|
||||
<directory>src/test/resources</directory>
|
||||
</testResource>
|
||||
</testResources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
|
|
|
@ -52,7 +52,7 @@ import static org.junit.Assert.assertTrue;
|
|||
*/
|
||||
public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
|
||||
|
||||
private static final String[] EXPECTED_BUILTIN_POLICY_PROVIDERS = {"test", "user", "role", "drools", "js", "time", "aggregate", "scope", "resource"};
|
||||
private static final String[] EXPECTED_BUILTIN_POLICY_PROVIDERS = {"test", "user", "role", "rules", "js", "time", "aggregate", "scope", "resource"};
|
||||
|
||||
@Before
|
||||
@Override
|
||||
|
|
|
@ -25,13 +25,15 @@ import org.keycloak.keys.KeyProvider;
|
|||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.migration.Migration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.RoleResource;
|
||||
|
@ -47,6 +49,7 @@ import org.keycloak.representations.idm.ClientTemplateRepresentation;
|
|||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
import static org.keycloak.testsuite.Assert.assertEquals;
|
||||
import static org.keycloak.testsuite.Assert.assertFalse;
|
||||
import static org.keycloak.testsuite.Assert.assertNames;
|
||||
|
@ -61,8 +64,10 @@ public class MigrationTest extends AbstractKeycloakTest {
|
|||
|
||||
public static final String MIGRATION = "Migration";
|
||||
public static final String MIGRATION2 = "Migration2";
|
||||
public static final String MIGRATION3 = "authorization";
|
||||
private RealmResource migrationRealm;
|
||||
private RealmResource migrationRealm2;
|
||||
private RealmResource migrationRealm3;
|
||||
private RealmResource masterRealm;
|
||||
|
||||
@Override
|
||||
|
@ -74,6 +79,7 @@ public class MigrationTest extends AbstractKeycloakTest {
|
|||
public void beforeMigrationTest() {
|
||||
migrationRealm = adminClient.realms().realm(MIGRATION);
|
||||
migrationRealm2 = adminClient.realms().realm(MIGRATION2);
|
||||
migrationRealm3 = adminClient.realms().realm(MIGRATION3);
|
||||
masterRealm = adminClient.realms().realm(MASTER);
|
||||
|
||||
//add migration realm to testRealmReps to make the migration removed after test
|
||||
|
@ -95,11 +101,11 @@ public class MigrationTest extends AbstractKeycloakTest {
|
|||
@Test
|
||||
@Migration(versionFrom = "2.2.1.Final")
|
||||
public void migration2_2_1Test() {
|
||||
testMigratedData();
|
||||
testMigrationTo2_3_0();
|
||||
testMigrationTo2_5_0();
|
||||
testMigrationTo2_5_1();
|
||||
}
|
||||
|
||||
|
||||
private void testMigratedData() {
|
||||
//master realm
|
||||
assertNames(masterRealm.roles().list(), "offline_access", "uma_authorization", "create-realm", "master-test-realm-role", "admin");
|
||||
|
@ -181,6 +187,10 @@ public class MigrationTest extends AbstractKeycloakTest {
|
|||
testDuplicateEmailSupport(masterRealm, migrationRealm);
|
||||
}
|
||||
|
||||
private void testMigrationTo2_5_1() {
|
||||
testDroolsToRulesPolicyTypeMigration();
|
||||
}
|
||||
|
||||
private void testLdapKerberosMigration_2_5_0() {
|
||||
RealmRepresentation realmRep = migrationRealm2.toRepresentation();
|
||||
List<ComponentRepresentation> components = migrationRealm2.components().query(realmRep.getId(), UserStorageProvider.class.getName());
|
||||
|
@ -214,6 +224,20 @@ public class MigrationTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void testDroolsToRulesPolicyTypeMigration() {
|
||||
List<ClientRepresentation> client = migrationRealm3.clients().findByClientId("photoz-restful-api");
|
||||
|
||||
assertEquals(1, client.size());
|
||||
|
||||
ClientRepresentation representation = client.get(0);
|
||||
|
||||
List<PolicyRepresentation> policies = migrationRealm3.clients().get(representation.getId()).authorization().policies().policies();
|
||||
|
||||
List<PolicyRepresentation> migratedRulesPolicies = policies.stream().filter(policyRepresentation -> "rules".equals(policyRepresentation.getType())).collect(Collectors.toList());
|
||||
|
||||
assertEquals(1, migratedRulesPolicies.size());
|
||||
}
|
||||
|
||||
private void testAuthorizationServices(RealmResource... realms) {
|
||||
for (RealmResource realm : realms) {
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
{
|
||||
"name": "Only Owner Policy",
|
||||
"description": "Defines that only the resource owner is allowed to do something",
|
||||
"type": "drools",
|
||||
"type": "rules",
|
||||
"logic": "POSITIVE",
|
||||
"decisionStrategy": "UNANIMOUS",
|
||||
"config": {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -489,7 +489,7 @@
|
|||
<profile>
|
||||
<id>auth-server-migration</id>
|
||||
<properties>
|
||||
<migration.import.file>src/test/resources/migration-test/migration-realm-${migrated.auth.server.version}.json</migration.import.file>
|
||||
<migration.import.file>target/test-classes/migration-test/migration-realm-${migrated.auth.server.version}.json</migration.import.file>
|
||||
<migration.import.props.previous>
|
||||
-Dkeycloak.migration.action=import
|
||||
-Dkeycloak.migration.provider=singleFile
|
||||
|
@ -572,7 +572,7 @@
|
|||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<migration.import.file>src/test/resources/migration-test/migration-realm-${migrated.auth.server.version}.json</migration.import.file>
|
||||
<migration.import.file>target/test-classes/migration-test/migration-realm-${migrated.auth.server.version}.json</migration.import.file>
|
||||
<migration.import.properties>
|
||||
-Dkeycloak.migration.action=import
|
||||
-Dkeycloak.migration.provider=singleFile
|
||||
|
@ -621,7 +621,7 @@
|
|||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<migration.import.file>src/test/resources/migration-test/migration-realm-${migrated.version.import.file.suffix}.json</migration.import.file>
|
||||
<migration.import.file>target/test-classes/migration-test/migration-realm-${migrated.version.import.file.suffix}.json</migration.import.file>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
|
|
|
@ -1142,7 +1142,7 @@ authz-policy-time-minute=Minute
|
|||
authz-policy-time-minute.tooltip=Defines the minute which the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current minute is between or equal to the two values you provided.
|
||||
|
||||
# Authz Drools Policy Detail
|
||||
authz-add-drools-policy=Add Drools Policy
|
||||
authz-add-drools-policy=Add Rules Policy
|
||||
authz-policy-drools-maven-artifact-resolve=Resolve
|
||||
authz-policy-drools-maven-artifact=Policy Maven Artifact
|
||||
authz-policy-drools-maven-artifact.tooltip=A Maven GAV pointing to an artifact from where the rules would be loaded from. Once you have provided the GAV, you can click *Resolve* to load both *Module* and *Session* fields.
|
||||
|
|
|
@ -1107,7 +1107,7 @@ authz-policy-time-minute=分
|
|||
authz-policy-time-minute.tooltip=ポリシーが許可される分を定義します。2番目のフィールドに値を入力して範囲を指定することもできます。この場合、現在の分が指定した2つの値の間にあるか、等しい場合のみ許可されます。
|
||||
|
||||
# Authz Drools Policy Detail
|
||||
authz-add-drools-policy=Drools ポリシーの追加
|
||||
authz-add-drools-policy=Rules ポリシーの追加
|
||||
authz-policy-drools-maven-artifact-resolve=解決
|
||||
authz-policy-drools-maven-artifact=ポリシー Maven アーティファクト
|
||||
authz-policy-drools-maven-artifact.tooltip=ルールの読み込む先となるアーティファクトを示す Maven GAV を設定します。GAV を提供し 「解決」 をクリックすることで、 「モジュール」 と 「セッション」 フィールドを読み込みます。
|
||||
|
|
|
@ -1102,7 +1102,7 @@ authz-policy-time-minute=Minut\u0117
|
|||
authz-policy-time-minute.tooltip=Nurodykite minut\u0119 iki kurios \u0161i taisykl\u0117 TENKINAMA. U\u017Epild\u017Eius antr\u0105j\u012F laukel\u012F, taisykl\u0117 bus TENKINAMA jei minut\u0117 patenka \u012F nurodyt\u0105 interval\u0105. Reik\u0161m\u0117s nurodomos imtinai.
|
||||
|
||||
# Authz Drools Policy Detail
|
||||
authz-add-drools-policy=Prid\u0117ti Drools taisykl\u0119
|
||||
authz-add-drools-policy=Prid\u0117ti Rules taisykl\u0119
|
||||
authz-policy-drools-maven-artifact-resolve=I\u0161spr\u0119sti
|
||||
authz-policy-drools-maven-artifact=Maven taisykl\u0117s artefaktas
|
||||
authz-policy-drools-maven-artifact.tooltip=Nuoroda \u012F Maven GAV artifakt\u0105 kuriame apra\u0161ytos taisykl\u0117s. Kai tik nurodysite GAV, galite paspausti *I\u0161spr\u0119sti* tam kad \u012Fkelti *Modulis* ir *Sesija* laukus.
|
||||
|
|
|
@ -1059,7 +1059,7 @@ authz-policy-time-not-on-after=Ikke p\u00E5 eller etter
|
|||
authz-policy-time-not-on-after.tooltip=Definerer tiden etter en policy M\u00C5 IKKE innvilges. Denne innvilges kun om gjeldende dato/tid er f\u00F8r eller lik denne verdien.
|
||||
|
||||
# Authz Drools Policy Detail
|
||||
authz-add-drools-policy=Legg til Drools policy
|
||||
authz-add-drools-policy=Legg til Rules policy
|
||||
authz-policy-drools-maven-artifact-resolve=L\u00F8s
|
||||
authz-policy-drools-maven-artifact=Policy for Maven artefakt.
|
||||
authz-policy-drools-maven-artifact.tooltip=Et Maven GAV som peker til et artefakt hvor reglene vil bli lastet fra. Med en gang du har gitt GAV kan du klikke *L\u00F8s* for \u00E5 laste felter for b\u00E5de *Modul* og *Sesjon*
|
||||
|
|
|
@ -771,7 +771,7 @@ authz-add-time-policy=Adicionar política de tempo
|
|||
authz-policy-time-not-on-after=Não em ou depois
|
||||
|
||||
# Authz Drools Policy Detail
|
||||
authz-add-drools-policy=Adicionar política Drools
|
||||
authz-add-drools-policy=Adicionar política Rules
|
||||
authz-policy-drools-maven-artifact-resolve=Resolver
|
||||
authz-policy-drools-maven-artifact=Artefato maven de política
|
||||
authz-policy-drools-module=Módulo
|
||||
|
|
|
@ -175,7 +175,7 @@ module.config(['$routeProvider', function ($routeProvider) {
|
|||
}
|
||||
},
|
||||
controller: 'ResourceServerPolicyCtrl'
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/drools/create', {
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/rules/create', {
|
||||
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-drools-detail.html',
|
||||
resolve: {
|
||||
realm: function (RealmLoader) {
|
||||
|
@ -186,7 +186,7 @@ module.config(['$routeProvider', function ($routeProvider) {
|
|||
}
|
||||
},
|
||||
controller: 'ResourceServerPolicyDroolsDetailCtrl'
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/drools/:id', {
|
||||
}).when('/realms/:realm/clients/:client/authz/resource-server/policy/rules/:id', {
|
||||
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-drools-detail.html',
|
||||
resolve: {
|
||||
realm: function (RealmLoader) {
|
||||
|
|
|
@ -743,7 +743,7 @@ module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route
|
|||
module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http, $route, realm, client, PolicyController) {
|
||||
PolicyController.onInit({
|
||||
getPolicyType : function() {
|
||||
return "drools";
|
||||
return "rules";
|
||||
},
|
||||
|
||||
onInit : function() {
|
||||
|
@ -754,7 +754,7 @@ module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http
|
|||
policy = $scope.policy;
|
||||
}
|
||||
|
||||
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/drools/resolveModules'
|
||||
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/rules/resolveModules'
|
||||
, policy).success(function(data) {
|
||||
$scope.drools.moduleNames = data;
|
||||
$scope.resolveSessions();
|
||||
|
@ -762,7 +762,7 @@ module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http
|
|||
}
|
||||
|
||||
$scope.resolveSessions = function() {
|
||||
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/drools/resolveSessions'
|
||||
$http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/rules/resolveSessions'
|
||||
, $scope.policy).success(function(data) {
|
||||
$scope.drools.moduleSessions = data;
|
||||
});
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
<table class="table kc-authz-table-expanded table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Associated Permissions</th>
|
||||
<th>Associated Policies</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies' | translate}}</a></li>
|
||||
<li data-ng-show="create">{{:: 'authz-add-drools-policy' | translate}}</li>
|
||||
<li data-ng-hide="create">Drools</li>
|
||||
<li data-ng-hide="create">Rules</li>
|
||||
<li data-ng-hide="create">{{originalPolicy.name}}</li>
|
||||
</ol>
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
<table class="table kc-authz-table-expanded table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dependent Permissions</th>
|
||||
<th>Dependent Permissions and Policies</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -91,7 +91,7 @@
|
|||
<span data-ng-show="policy.dependentPolicies && !policy.dependentPolicies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
|
||||
<ul ng-repeat="dep in policy.dependentPolicies" data-ng-show="policy.dependentPolicies.length > 0">
|
||||
<li>
|
||||
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{dep.type}}/{{dep.id}}">{{dep.name}}</a>
|
||||
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/{{dep.type == 'scope' || dep.type == 'resource' ? 'permission' : 'policy'}}/{{dep.type}}/{{dep.id}}">{{dep.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
|
Loading…
Reference in a new issue