Skip to content
Snippets Groups Projects
Verified Commit ec6662b6 authored by Dominik Frantisek Bucik's avatar Dominik Frantisek Bucik
Browse files

feat: :guitar: Pass in ACRs onlyAllowed and blocked IdPs

Lists are read from client entry int DB. Also need to set
`proxy.blocked_idps_enabled=true` and/or
`proxy.only_allowed_idps_enabled=true` in the main config file to enable
passing these alists to proxyIdP

BREAKING CHANGE: requires database update (see v18.0.0.sql script)
parent 702c58ce
Branches
Tags
1 merge request!387LS AAI template, passing IdP filters via ACRs
Pipeline #412957 passed
Showing
with 204 additions and 17 deletions
...@@ -302,3 +302,13 @@ CREATE TABLE IF NOT EXISTS device_code_request_parameter ( ...@@ -302,3 +302,13 @@ CREATE TABLE IF NOT EXISTS device_code_request_parameter (
param VARCHAR(2048), param VARCHAR(2048),
val VARCHAR(2048) val VARCHAR(2048)
); );
CREATE TABLE IF NOT EXISTS client_only_allowed_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_blocked_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_only_allowed_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_blocked_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
alter table client_only_allowed_idps
add constraint client_only_allowed_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
alter table client_blocked_idps
add constraint client_blocked_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
\ No newline at end of file
...@@ -205,6 +205,16 @@ CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( ...@@ -205,6 +205,16 @@ CREATE TABLE IF NOT EXISTS client_claims_redirect_uri (
redirect_uri VARCHAR(2048) redirect_uri VARCHAR(2048)
); );
CREATE TABLE IF NOT EXISTS client_only_allowed_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_blocked_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS refresh_token ( CREATE TABLE IF NOT EXISTS refresh_token (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY,
token_value VARCHAR(4096), token_value VARCHAR(4096),
...@@ -471,3 +481,13 @@ alter table whitelisted_site_scope ...@@ -471,3 +481,13 @@ alter table whitelisted_site_scope
add constraint whitelisted_site_scope_whitelisted_site_id_fk add constraint whitelisted_site_scope_whitelisted_site_id_fk
foreign key (owner_id) references whitelisted_site (id) foreign key (owner_id) references whitelisted_site (id)
on update cascade on delete cascade; on update cascade on delete cascade;
alter table client_only_allowed_idps
add constraint client_only_allowed_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
alter table client_blocked_idps
add constraint client_blocked_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
\ No newline at end of file
CREATE TABLE IF NOT EXISTS client_only_allowed_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_blocked_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
alter table client_only_allowed_idps
add constraint client_only_allowed_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
alter table client_blocked_idps
add constraint client_blocked_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
\ No newline at end of file
...@@ -209,6 +209,16 @@ CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( ...@@ -209,6 +209,16 @@ CREATE TABLE IF NOT EXISTS client_claims_redirect_uri (
redirect_uri VARCHAR(2048) redirect_uri VARCHAR(2048)
); );
CREATE TABLE IF NOT EXISTS client_only_allowed_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_blocked_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS refresh_token ( CREATE TABLE IF NOT EXISTS refresh_token (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
token_value VARCHAR(4096), token_value VARCHAR(4096),
...@@ -435,6 +445,16 @@ alter table client_scope ...@@ -435,6 +445,16 @@ alter table client_scope
foreign key (owner_id) references client_details (id) foreign key (owner_id) references client_details (id)
on update cascade on delete cascade; on update cascade on delete cascade;
alter table client_only_allowed_idps
add constraint client_only_allowed_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
alter table client_blocked_idps
add constraint client_blocked_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
alter table device_code alter table device_code
add constraint device_code_client_details_id_fk add constraint device_code_client_details_id_fk
foreign key (client_id) references client_details (client_id) foreign key (client_id) references client_details (client_id)
......
CREATE TABLE IF NOT EXISTS client_only_allowed_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
CREATE TABLE IF NOT EXISTS client_blocked_idps (
owner_id BIGINT,
idp_entity_id VARCHAR(512)
);
alter table client_only_allowed_idps
add constraint client_only_allowed_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
alter table client_blocked_idps
add constraint client_blocked_idps_client_details_id_fk
foreign key (owner_id) references client_details (id)
on update cascade on delete cascade;
\ No newline at end of file
...@@ -98,6 +98,8 @@ ...@@ -98,6 +98,8 @@
<prop key="proxy.extSource.name"/> <prop key="proxy.extSource.name"/>
<prop key="proxy.base.url"/> <prop key="proxy.base.url"/>
<prop key="proxy.add_client_id_to_acrs">false</prop> <prop key="proxy.add_client_id_to_acrs">false</prop>
<prop key="proxy.only_allowed_idps_enabled">false</prop>
<prop key="proxy.blocked_idps_enabled">false</prop>
<!-- OIDC STUFF --> <!-- OIDC STUFF -->
<prop key="jwk">file:///etc/perun/perun-oidc-keystore.jwks</prop> <prop key="jwk">file:///etc/perun/perun-oidc-keystore.jwks</prop>
<prop key="id_token.scopes">openid,profile,email,phone,address</prop> <prop key="id_token.scopes">openid,profile,email,phone,address</prop>
...@@ -128,6 +130,7 @@ ...@@ -128,6 +130,7 @@
<prop key="filter.stats.spIdColumnName">spId</prop> <prop key="filter.stats.spIdColumnName">spId</prop>
<prop key="sentry.config.location"/> <prop key="sentry.config.location"/>
<prop key="ga4gh.tokenExchange.brokerUrl"/> <prop key="ga4gh.tokenExchange.brokerUrl"/>
</props> </props>
</property> </property>
</bean> </bean>
...@@ -476,6 +479,8 @@ ...@@ -476,6 +479,8 @@
<property name="krbTokenExchangeRequiredScopes" value="#{'${token-exchange.kerberos.requiredScopes}'.split('\s*,\s*')}"/> <property name="krbTokenExchangeRequiredScopes" value="#{'${token-exchange.kerberos.requiredScopes}'.split('\s*,\s*')}"/>
<property name="requesterIdPrefix" value="${saml.requester-id.prefix}"/> <property name="requesterIdPrefix" value="${saml.requester-id.prefix}"/>
<property name="logRequestsEnabled" value="${logRequestsEnabled}"/> <property name="logRequestsEnabled" value="${logRequestsEnabled}"/>
<property name="onlyAllowedIdpsEnabled" value="${proxy.only_allowed_idps_enabled}"/>
<property name="blockedIdpsEnabled" value="${proxy.blocked_idps_enabled}"/>
</bean> </bean>
<bean id="facilityAttrsConfig" class="cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig"> <bean id="facilityAttrsConfig" class="cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig">
......
...@@ -338,6 +338,18 @@ public class ClientDetailsEntity implements ClientDetails { ...@@ -338,6 +338,18 @@ public class ClientDetailsEntity implements ClientDetails {
@Column(name = "parent_client_id") @Column(name = "parent_client_id")
private Long parentClientId; private Long parentClientId;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "client_only_allowed_idps", joinColumns = @JoinColumn(name = "owner_id"))
@Column(name = "idp_entity_id")
@CascadeOnDelete
private Set<String> onlyAllowedIdps;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "client_blocked_idps", joinColumns = @JoinColumn(name = "owner_id"))
@Column(name = "idp_entity_id")
@CascadeOnDelete
private Set<String> blockedIdps;
@Transient @Transient
private Map<String, Object> additionalInformation = new HashMap<>(); private Map<String, Object> additionalInformation = new HashMap<>();
......
package cz.muni.ics.oidc.saml; package cz.muni.ics.oidc.saml;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.DeviceCode; import cz.muni.ics.oauth2.model.DeviceCode;
import cz.muni.ics.oauth2.repository.impl.DeviceCodeRepository; import cz.muni.ics.oauth2.repository.impl.DeviceCodeRepository;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oidc.models.Facility; import cz.muni.ics.oidc.models.Facility;
import cz.muni.ics.oidc.models.PerunAttributeValue; import cz.muni.ics.oidc.models.PerunAttributeValue;
import cz.muni.ics.oidc.server.adapters.PerunAdapter; import cz.muni.ics.oidc.server.adapters.PerunAdapter;
...@@ -39,10 +41,12 @@ import java.util.Set; ...@@ -39,10 +41,12 @@ import java.util.Set;
import static cz.muni.ics.oauth2.web.endpoint.DeviceEndpoint.PATH_DEVICE_AUTHORIZE; import static cz.muni.ics.oauth2.web.endpoint.DeviceEndpoint.PATH_DEVICE_AUTHORIZE;
import static cz.muni.ics.oauth2.web.endpoint.DeviceEndpoint.USER_CODE; import static cz.muni.ics.oauth2.web.endpoint.DeviceEndpoint.USER_CODE;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.AARC_IDP_HINT; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.AARC_IDP_HINT;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.BLOCKED_IDPS_ACR_PREFIX;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.CLIENT_ID_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.CLIENT_ID_PREFIX;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.EFILTER_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.EFILTER_PREFIX;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.FILTER_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.FILTER_PREFIX;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.IDP_ENTITY_ID_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.IDP_ENTITY_ID_PREFIX;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.ONLY_ALLOWED_IDPS_ACR_PREFIX;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_CLIENT_ID; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_CLIENT_ID;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_MAX_AGE; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_MAX_AGE;
import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_PROMPT; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_PROMPT;
...@@ -55,18 +59,21 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { ...@@ -55,18 +59,21 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint {
private final FacilityAttrsConfig facilityAttrsConfig; private final FacilityAttrsConfig facilityAttrsConfig;
private final SamlProperties samlProperties; private final SamlProperties samlProperties;
private final DeviceCodeRepository deviceCodeRepository; private final DeviceCodeRepository deviceCodeRepository;
private final ClientDetailsEntityService clientDetailsEntityService;
public PerunSamlEntryPoint(PerunAdapter perunAdapter, public PerunSamlEntryPoint(PerunAdapter perunAdapter,
PerunOidcConfig config, PerunOidcConfig config,
FacilityAttrsConfig facilityAttrsConfig, FacilityAttrsConfig facilityAttrsConfig,
SamlProperties samlProperties, SamlProperties samlProperties,
DeviceCodeRepository deviceCodeRepository) DeviceCodeRepository deviceCodeRepository,
{ ClientDetailsEntityService clientDetailsEntityService
) {
this.perunAdapter = perunAdapter; this.perunAdapter = perunAdapter;
this.config = config; this.config = config;
this.facilityAttrsConfig = facilityAttrsConfig; this.facilityAttrsConfig = facilityAttrsConfig;
this.samlProperties = samlProperties; this.samlProperties = samlProperties;
this.deviceCodeRepository = deviceCodeRepository; this.deviceCodeRepository = deviceCodeRepository;
this.clientDetailsEntityService = clientDetailsEntityService;
} }
@Override @Override
...@@ -176,12 +183,12 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { ...@@ -176,12 +183,12 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint {
} }
private void processPrompt(Map<String, String> requestParameters, WebSSOProfileOptions options) { private void processPrompt(Map<String, String> requestParameters, WebSSOProfileOptions options) {
if (PerunSamlUtils.needsReAuthByPrompt(requestParameters.getOrDefault(PARAM_PROMPT, null))) { String prompt = requestParameters.getOrDefault(PARAM_PROMPT, "");
log.debug("Transformed prompt parameter ({}) to SAML forceAuthn=true", if (PerunSamlUtils.needsReAuthByPrompt(prompt)) {
requestParameters.get(PARAM_PROMPT)); log.debug("Transformed prompt parameter ({}) to SAML forceAuthn=true", prompt);
options.setForceAuthN(true); options.setForceAuthN(true);
} }
if ("none".equalsIgnoreCase(requestParameters.getOrDefault(PARAM_PROMPT, ""))) { if ("none".equalsIgnoreCase(prompt)) {
log.debug("Detected prompt=none, translating to 'isPassive=true' in SAML"); log.debug("Detected prompt=none, translating to 'isPassive=true' in SAML");
options.setPassive(true); options.setPassive(true);
} }
...@@ -203,25 +210,48 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { ...@@ -203,25 +210,48 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint {
acrs = convertAcrValuesToList(acrValues); acrs = convertAcrValuesToList(acrValues);
} }
if (!hasAcrForcingIdp(acrs)) { String clientId = requestParameters.getOrDefault(AuthProcFilterConstants.PARAM_CLIENT_ID, null);
String clientId = requestParameters.getOrDefault(AuthProcFilterConstants.PARAM_CLIENT_ID, null); if (StringUtils.hasText(clientId)) {
if (clientId != null) { // ADD FILTER AND E-FILTER
if (config.isAskPerunForIdpFiltersEnabled() && !hasAcrForcingIdp(acrs)) {
String idpFilter = extractIdpFilterForRp(clientId); String idpFilter = extractIdpFilterForRp(clientId);
if (idpFilter != null) { if (idpFilter != null) {
log.debug("Added IdP filter as SAML AuthnContextClassRef ({})", idpFilter); log.debug("Added IdP filter as SAML AuthnContextClassRef ({})", idpFilter);
acrs.add(idpFilter); acrs.add(idpFilter);
} }
} }
}
if (StringUtils.hasText(requestParameters.getOrDefault(PARAM_CLIENT_ID, "")) && config.isAddClientIdToAcrs()) { ClientDetailsEntity client = clientDetailsEntityService.loadClientByClientId(clientId);
String clientIdAcr = CLIENT_ID_PREFIX + requestParameters.get(PARAM_CLIENT_ID); if (client != null) {
log.debug("Adding client_id ACR ({}) to list of AuthnContextClassRefs for purposes" + // ADD BLOCKED IdPs
" of displaying service name on the wayf", clientIdAcr); String blockedIdps = getBlockedIdpsAcr(client);
acrs.add(clientIdAcr); log.debug("blockedIdps ({})", blockedIdps);
if (StringUtils.hasText(blockedIdps)) {
String acr = BLOCKED_IDPS_ACR_PREFIX + blockedIdps;
log.debug("Added blockedIdps as SAML AuthnContextClassRef ({})", acr);
acrs.add(acr);
}
// ADD ONLY ALLOWED IdPs
String onlyAllowedIdps = getOnlyAllowedIdpsAcr(client);
log.debug("allowedIdps ({})", onlyAllowedIdps);
if (StringUtils.hasText(onlyAllowedIdps)) {
String acr = ONLY_ALLOWED_IDPS_ACR_PREFIX + onlyAllowedIdps;
log.debug("Added onlyAllowedIdps as SAML AuthnContextClassRef ({})", acr);
acrs.add(acr);
}
}
// ADD CLIENT_ID
if (config.isAddClientIdToAcrs()) {
String clientIdAcr = CLIENT_ID_PREFIX + requestParameters.get(PARAM_CLIENT_ID);
log.debug("Adding client_id ACR ({}) to list of AuthnContextClassRefs for purposes" +
" of displaying service name on the wayf", clientIdAcr);
acrs.add(clientIdAcr);
}
} }
if (acrs.size() > 0) { if (!acrs.isEmpty()) {
processAcrs(acrs); processAcrs(acrs);
options.setAuthnContexts(acrs); options.setAuthnContexts(acrs);
log.debug("Transformed acr_values ({}) to SAML AuthnContextClassRef ({})", log.debug("Transformed acr_values ({}) to SAML AuthnContextClassRef ({})",
...@@ -237,7 +267,7 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { ...@@ -237,7 +267,7 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint {
} }
String clientId = requestParameters.getOrDefault(PARAM_CLIENT_ID, null); String clientId = requestParameters.getOrDefault(PARAM_CLIENT_ID, null);
if (StringUtils.hasText(clientId)) { if (StringUtils.hasText(clientId)) {
log.debug("Adding ClientID ({}) to SAML RequesterIDs", requestParameters.get(PARAM_CLIENT_ID)); log.debug("Adding ClientID ({}) to SAML RequesterIDs", clientId);
Set<String> requesterIds = options.getRequesterIds(); Set<String> requesterIds = options.getRequesterIds();
if (requesterIds == null) { if (requesterIds == null) {
requesterIds = new HashSet<>(); requesterIds = new HashSet<>();
...@@ -356,4 +386,26 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { ...@@ -356,4 +386,26 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint {
return result; return result;
} }
private String getOnlyAllowedIdpsAcr(ClientDetailsEntity client) {
String result = null;
if (config.isOnlyAllowedIdpsEnabled()) {
Set<String> idps = client.getOnlyAllowedIdps();
if (idps != null && !idps.isEmpty()) {
result = String.join(";", idps);
}
}
return result;
}
private String getBlockedIdpsAcr(ClientDetailsEntity client) {
String result = null;
if (config.isBlockedIdpsEnabled()) {
Set<String> idps = client.getBlockedIdps();
if (idps != null && !idps.isEmpty()) {
result = String.join(";", idps);
}
}
return result;
}
} }
package cz.muni.ics.oidc.server.configurations; package cz.muni.ics.oidc.server.configurations;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
......
...@@ -86,6 +86,10 @@ public class PerunOidcConfig implements InitializingBean { ...@@ -86,6 +86,10 @@ public class PerunOidcConfig implements InitializingBean {
private Set<String> krbTokenExchangeRequiredScopes; private Set<String> krbTokenExchangeRequiredScopes;
private boolean onlyAllowedIdpsEnabled = false;
private boolean blockedIdpsEnabled = false;
@Autowired @Autowired
private ServletContext servletContext; private ServletContext servletContext;
...@@ -169,6 +173,8 @@ public class PerunOidcConfig implements InitializingBean { ...@@ -169,6 +173,8 @@ public class PerunOidcConfig implements InitializingBean {
log.info("Localization files path: {}", localizationFilesPath); log.info("Localization files path: {}", localizationFilesPath);
log.info("Email contact: {}", emailContact); log.info("Email contact: {}", emailContact);
log.info("Sentry enabled: {}", StringUtils.hasText(sentryConfigFileLocation)); log.info("Sentry enabled: {}", StringUtils.hasText(sentryConfigFileLocation));
log.info("OnlyAllowedIdPs ACR enabled: {}", onlyAllowedIdpsEnabled);
log.info("BlockedIdPs ACR enabled: {}", blockedIdpsEnabled);
log.info("Perun OIDC version: {}", getPerunOIDCVersion()); log.info("Perun OIDC version: {}", getPerunOIDCVersion());
} }
} }
......
...@@ -32,6 +32,9 @@ public interface AuthProcFilterConstants { ...@@ -32,6 +32,9 @@ public interface AuthProcFilterConstants {
String FILTER_PREFIX = "urn:cesnet:proxyidp:filter:"; String FILTER_PREFIX = "urn:cesnet:proxyidp:filter:";
String EFILTER_PREFIX = "urn:cesnet:proxyidp:efilter:"; String EFILTER_PREFIX = "urn:cesnet:proxyidp:efilter:";
String ONLY_ALLOWED_IDPS_ACR_PREFIX = "urn:cesnet:proxyidp:only_allowed_idps:";
String BLOCKED_IDPS_ACR_PREFIX = "urn:cesnet:proxyidp:blocked_idps:";
String SAML_EPUID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.13"; String SAML_EPUID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.13";
String SAML_EPPN = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"; String SAML_EPPN = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6";
String SAML_EPTID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.10"; String SAML_EPTID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.10";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment