Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
dariah
dariahsp
Commits
5bd2d4f4
Commit
5bd2d4f4
authored
Oct 30, 2020
by
Gradl, Tobias
Browse files
1: Rebase on newer OpenSAML
Task-Url:
#1
parent
2066778e
Pipeline
#17518
passed with stage
in 1 minute and 47 seconds
Changes
10
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/config/BaseUrl.java
0 → 100644
View file @
5bd2d4f4
package
eu.dariah.de.dariahsp.sample.config
;
import
java.net.URI
;
import
java.net.URISyntaxException
;
import
lombok.Data
;
@Data
public
class
BaseUrl
{
private
final
String
url
;
public
String
getAbsoluteUrl
(
String
relativeComponent
)
throws
URISyntaxException
{
String
composedUrl
=
String
.
format
(
"%s/%s"
,
this
.
url
,
relativeComponent
);
return
new
URI
(
composedUrl
).
normalize
().
toString
();
}
}
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/config/SamlProperties.java
View file @
5bd2d4f4
package
eu.dariah.de.dariahsp.sample.config
;
import
org.apache.http.client.HttpClient
;
import
java.time.Duration
;
import
java.util.ArrayList
;
import
java.util.List
;
import
org.opensaml.saml.common.xml.SAMLConstants
;
import
org.pac4j.saml.util.SAML2HttpClientBuilder
;
import
lombok.Getter
;
import
lombok.Setter
;
...
...
@@ -30,8 +33,10 @@ public class SamlProperties {
@Getter
@Setter
public
class
SpProperties
{
private
String
externalMetadata
;
private
String
metadataResource
;
private
boolean
generateIfNotExists
;
private
int
maxAuthAge
=
3600
;
private
String
baseUrl
=
"http://localhost:8080"
;
private
String
entityId
;
private
int
httpClientTimoutMs
=
2000
;
private
boolean
signMetadata
;
...
...
@@ -52,10 +57,19 @@ public class SamlProperties {
List
<
String
>
p
=
supportedProtocols
;
if
(
p
==
null
)
{
p
=
new
ArrayList
<>();
p
.
add
(
SAMLConstants
.
SAML20_NS
);
p
.
add
(
SAMLConstants
.
SAML20
P
_NS
);
}
return
p
;
}
public
int
getMaxAuthAge
()
{
return
maxAuthAge
<=
0
?
Integer
.
MAX_VALUE
:
maxAuthAge
;
}
public
HttpClient
getHttpClient
()
{
SAML2HttpClientBuilder
httpClient
=
new
SAML2HttpClientBuilder
();
httpClient
.
setConnectionTimeout
(
Duration
.
ofSeconds
(
httpClientTimoutMs
));
httpClient
.
setSocketTimeout
(
Duration
.
ofSeconds
(
httpClientTimoutMs
));
return
httpClient
.
build
();
}
}
}
\ No newline at end of file
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/config/SecurityConfig.java
View file @
5bd2d4f4
package
eu.dariah.de.dariahsp.sample.config
;
import
java.time.Duration
;
import
java.io.FileNotFoundException
;
import
java.net.URISyntaxException
;
import
java.nio.file.Files
;
import
java.nio.file.Paths
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.Optional
;
import
org.opensaml.saml.common.xml.SAMLConstants
;
import
org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer
;
import
org.pac4j.core.client.Client
;
import
org.pac4j.core.client.Clients
;
import
org.pac4j.core.config.Config
;
import
org.pac4j.http.client.indirect.FormClient
;
import
org.pac4j.saml.client.SAML2Client
;
import
org.pac4j.saml.config.SAML2Configuration
;
import
org.pac4j.saml.util.SAML2HttpClientBuilder
;
import
org.pac4j.springframework.annotation.AnnotationConfig
;
import
org.pac4j.springframework.component.ComponentConfig
;
import
org.springframework.boot.context.properties.ConfigurationProperties
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.context.annotation.Import
;
import
org.springframework.core.io.FileSystemResource
;
import
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
;
import
eu.dariah.de.dariahsp.sample.CustomAuthorizer
;
import
eu.dariah.de.dariahsp.sample.authenticator.LocalUsernamePasswordAuthenticator
;
import
lombok.Data
;
import
lombok.extern.slf4j.Slf4j
;
@Data
@Slf4j
@Configuration
@ConfigurationProperties
(
prefix
=
"auth"
)
@Import
({
ComponentConfig
.
class
,
AnnotationConfig
.
class
})
...
...
@@ -36,19 +40,49 @@ public class SecurityConfig {
private
final
SamlProperties
saml
=
new
SamlProperties
();
@Bean
LocalUsernamePasswordAuthenticator
localUsernamePasswordAuthenticator
()
{
public
Optional
<
LocalUsernamePasswordAuthenticator
>
localUsernamePasswordAuthenticator
()
{
if
(!
local
.
isEnabled
())
{
return
Optional
.
empty
();
}
LocalUsernamePasswordAuthenticator
localAuthenticator
=
new
LocalUsernamePasswordAuthenticator
();
localAuthenticator
.
setEncoder
(
new
BCryptPasswordEncoder
());
if
(
local
.
isEnabled
())
{
localAuthenticator
.
setLocalUserConfigurations
(
local
.
getUsers
());
localAuthenticator
.
setLocalUserConfigurations
(
local
.
getUsers
());
return
Optional
.
of
(
localAuthenticator
);
}
@Bean
public
BaseUrl
baseUrl
()
{
return
new
BaseUrl
(
saml
.
getSp
().
getBaseUrl
());
}
@Bean
@SuppressWarnings
(
"rawtypes"
)
public
Config
config
()
throws
URISyntaxException
{
List
<
Client
>
clients
=
new
ArrayList
<>();
Optional
<
SAML2Client
>
samlClient
=
getSamlClient
();
Optional
<
FormClient
>
formClient
=
getFormClient
();
if
(
samlClient
.
isPresent
())
{
clients
.
add
(
samlClient
.
get
());
}
return
localAuthenticator
;
}
if
(
formClient
.
isPresent
())
{
clients
.
add
(
formClient
.
get
());
}
final
Config
config
=
new
Config
(
new
Clients
(
baseUrl
().
getAbsoluteUrl
(
"/callback"
),
clients
));
config
.
addAuthorizer
(
"admin"
,
new
RequireAnyRoleAuthorizer
(
"ROLE_ADMIN"
));
config
.
addAuthorizer
(
"custom"
,
new
CustomAuthorizer
());
//config.addMatcher("excludedPath", new PathMatcher().excludeRegex("^/*$"));
return
config
;
}
@Bean
Config
config
()
{
private
Optional
<
SAML2Client
>
getSamlClient
()
throws
URISyntaxException
{
if
(!
saml
.
isEnabled
())
{
return
Optional
.
empty
();
}
final
SAML2Configuration
cfg
=
new
SAML2Configuration
();
// Keystore
...
...
@@ -61,55 +95,37 @@ public class SecurityConfig {
cfg
.
setIdentityProviderMetadataPath
(
saml
.
getMetadata
().
getUrl
());
// SP Metadata
if
(
saml
.
getSp
().
getExternalMetadata
()!=
null
)
{
cfg
.
setServiceProviderMetadataPath
(
saml
.
getSp
().
getExternalMetadata
());
}
else
{
cfg
.
setServiceProviderMetadataPath
(
"/tmp/sp_metadata.xml"
);
}
if
(
saml
.
getSp
().
getMaxAuthAge
()<=
0
)
{
cfg
.
setMaximumAuthenticationLifetime
(
Integer
.
MAX_VALUE
);
if
(
saml
.
getSp
().
getMetadataResource
()!=
null
)
{
// metadata is not automatically written to filesystem, but created in-memory
if
(!
Files
.
exists
(
Paths
.
get
(
saml
.
getSp
().
getMetadataResource
())))
{
log
.
warn
(
"Configured SP metadata resource does not exist"
,
new
FileNotFoundException
(
saml
.
getSp
().
getMetadataResource
()));
}
else
{
cfg
.
setServiceProviderMetadataPath
(
saml
.
getSp
().
getMetadataResource
());
}
}
else
{
cfg
.
setMaximumAuthenticationLifetime
(
saml
.
getSp
().
getMaxAuthAge
()
);
log
.
info
(
"SP metadata resource is not configured (auth.saml.sp.metadataResource); metadata will be generated and served in-memory"
);
}
cfg
.
setMaximumAuthenticationLifetime
(
saml
.
getSp
().
getMaxAuthAge
());
cfg
.
setSignatureAlgorithms
(
saml
.
getSp
().
getSigningMethods
());
cfg
.
setSignatureReferenceDigestMethods
(
saml
.
getSp
().
getDigestMethods
());
cfg
.
setServiceProviderEntityId
(
saml
.
getSp
().
getEntityId
());
cfg
.
setSpLogoutRequestSigned
(
saml
.
getSp
().
isLogoutRequestSigned
());
cfg
.
setWantsAssertionsSigned
(
saml
.
getSp
().
isWantsAssertionsSigned
());
cfg
.
setWantsResponsesSigned
(
saml
.
getSp
().
isWantsResponsesSigned
());
cfg
.
setAuthnRequestSigned
(
saml
.
getSp
().
isAuthnRequestSigned
());
cfg
.
setSignMetadata
(
saml
.
getSp
().
isSignMetadata
());
cfg
.
setSupportedProtocols
(
saml
.
getSp
().
getSupportedProtocols
());
cfg
.
setHttpClient
(
saml
.
getSp
().
getHttpClient
());
SAML2Client
samlClient
=
new
SAML2Client
(
cfg
);
// HTTP
final
FormClient
formClient
=
new
FormClient
(
"/loginForm"
,
localUsernamePasswordAuthenticator
());
final
Config
config
=
new
Config
(
new
Clients
(
"/callback"
,
samlClient
,
formClient
));
config
.
addAuthorizer
(
"admin"
,
new
RequireAnyRoleAuthorizer
(
"ROLE_ADMIN"
));
config
.
addAuthorizer
(
"custom"
,
new
CustomAuthorizer
());
//config.addMatcher("excludedPath", new PathMatcher().excludeRegex("^/*$"));
return
config
;
return
Optional
.
of
(
new
SAML2Client
(
cfg
));
}
private
Optional
<
FormClient
>
getFormClient
()
{
Optional
<
LocalUsernamePasswordAuthenticator
>
localUsernamePasswordAuthenticator
=
localUsernamePasswordAuthenticator
();
if
(
localUsernamePasswordAuthenticator
.
isPresent
())
{
return
Optional
.
of
(
new
FormClient
(
"/login"
,
localUsernamePasswordAuthenticator
.
get
()));
}
return
Optional
.
empty
();
}
}
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/config/WebSecurityConfig.java
View file @
5bd2d4f4
package
eu.dariah.de.dariahsp.sample.config
;
import
java.util.List
;
import
java.util.stream.Collectors
;
import
org.pac4j.core.client.Client
;
import
org.pac4j.core.config.Config
;
import
org.pac4j.springframework.security.web.CallbackFilter
;
import
org.pac4j.springframework.security.web.LogoutFilter
;
...
...
@@ -17,29 +21,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
@EnableWebSecurity
public
class
WebSecurityConfig
{
@Configuration
@Order
(
5
)
public
static
class
FormWebSecurityConfigurationAdapter
extends
WebSecurityConfigurerAdapter
{
@Autowired
private
Config
config
;
protected
void
configure
(
final
HttpSecurity
http
)
throws
Exception
{
//final SecurityFilter filter = new SecurityFilter(config, "DirectBasicAuthClient,AnonymousClient");
final
SecurityFilter
filter
=
new
SecurityFilter
(
config
,
"FormClient"
);
http
.
antMatcher
(
"/form/**"
)
.
authorizeRequests
().
anyRequest
().
authenticated
()
.
and
()
.
exceptionHandling
().
authenticationEntryPoint
(
new
Pac4jEntryPoint
(
config
,
"FormClient"
))
.
and
()
.
addFilterBefore
(
filter
,
BasicAuthenticationFilter
.
class
)
.
sessionManagement
().
sessionCreationPolicy
(
SessionCreationPolicy
.
ALWAYS
);
}
}
@Configuration
@Order
(
7
)
public
static
class
Saml2WebSecurityConfigurationAdapter
extends
WebSecurityConfigurerAdapter
{
...
...
@@ -49,51 +31,34 @@ public class WebSecurityConfig {
protected
void
configure
(
final
HttpSecurity
http
)
throws
Exception
{
final
SecurityFilter
filter
=
new
SecurityFilter
(
config
,
"Saml2Client"
);
List
<
String
>
enabledClientNames
=
config
.
getClients
().
findAllClients
().
stream
()
.
map
(
Client:
:
getName
)
.
collect
(
Collectors
.
toList
());
final
SecurityFilter
filter
=
new
SecurityFilter
(
config
,
enabledClientNames
.
stream
().
collect
(
Collectors
.
joining
(
","
)));
// TODO: What happens if there is no client? Everything open or 403??
http
.
antMatcher
(
"/saml/**"
)
.
authorizeRequests
()
.
antMatchers
(
"/saml/admin.html"
).
hasRole
(
"ADMIN"
)
.
antMatchers
(
"/saml/**"
).
authenticated
()
.
and
()
.
addFilterBefore
(
filter
,
BasicAuthenticationFilter
.
class
)
.
sessionManagement
().
sessionCreationPolicy
(
SessionCreationPolicy
.
ALWAYS
);
}
}
/* @Configuration
@Order(7)
public static class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private Config config;
protected void configure(final HttpSecurity http) throws Exception {
final SecurityFilter filter = new SecurityFilter(config, "Saml2Client, FormClient");
http
.antMatcher("/saml/**")
.authorizeRequests()
.antMatchers("/saml/admin.html").hasRole("ADMIN")
.antMatchers("/saml/**").authenticated()
.and()
.addFilterBefore(filter, BasicAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
.
requestMatchers
()
.
antMatchers
(
"/saml/**"
,
"/form/**"
)
.
and
()
.
authorizeRequests
()
.
antMatchers
(
"/saml/admin.html"
).
hasRole
(
"ADMIN"
)
.
antMatchers
(
"/saml/**"
).
authenticated
()
.
and
()
.
addFilterBefore
(
filter
,
BasicAuthenticationFilter
.
class
)
.
sessionManagement
().
sessionCreationPolicy
(
SessionCreationPolicy
.
ALWAYS
);
if
(
enabledClientNames
.
get
(
0
).
equals
(
"FormClient"
))
{
http
.
exceptionHandling
().
authenticationEntryPoint
(
new
Pac4jEntryPoint
(
config
,
"FormClient"
));
}
http
.antMatcher("/form/**")
.authorizeRequests().anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(new Pac4jEntryPoint(config, "FormClient"))
.and()
.addFilterBefore(filter, BasicAuthenticationFilter.class)
http
.
sessionManagement
().
sessionCreationPolicy
(
SessionCreationPolicy
.
ALWAYS
);
}
}
*/
}
@Configuration
@Order
(
15
)
...
...
@@ -119,11 +84,9 @@ public class WebSecurityConfig {
http
.
authorizeRequests
()
.
antMatchers
(
"/cas/**"
).
authenticated
()
.
anyRequest
().
permitAll
()
.
authorizeRequests
().
anyRequest
().
permitAll
()
.
and
()
.
exceptionHandling
().
authenticationEntryPoint
(
new
Pac4jEntryPoint
(
config
,
"
Cas
Client"
))
.
exceptionHandling
().
authenticationEntryPoint
(
new
Pac4jEntryPoint
(
config
,
"
Form
Client"
))
.
and
()
.
addFilterBefore
(
callbackFilter
,
BasicAuthenticationFilter
.
class
)
.
addFilterBefore
(
logoutFilter
,
CallbackFilter
.
class
)
...
...
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/controller/SampleController.java
View file @
5bd2d4f4
...
...
@@ -2,6 +2,8 @@ package eu.dariah.de.dariahsp.sample.controller;
import
java.util.Map
;
import
javax.websocket.server.PathParam
;
import
org.pac4j.core.client.Client
;
import
org.pac4j.core.config.Config
;
import
org.pac4j.core.context.JEEContext
;
...
...
@@ -19,6 +21,7 @@ import org.springframework.http.ResponseEntity;
import
org.springframework.stereotype.Controller
;
import
org.springframework.ui.Model
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.ResponseBody
;
...
...
@@ -26,7 +29,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import
eu.dariah.de.dariahsp.sample.Constants
;
import
eu.dariah.de.dariahsp.sample.error.SAML2MetadataNotFoundException
;
import
eu.dariah.de.dariahsp.sample.metadata.MetadataHelper
;
import
javassist
.NotFoundException
;
import
eu.dariah.de.dariahsp.sample.error
.NotFoundException
;
@Controller
public
class
SampleController
{
...
...
@@ -54,7 +57,7 @@ public class SampleController {
return
protectedIndex
(
map
);
}
@RequestMapping
(
"/login
Form
"
)
@RequestMapping
(
"/login"
)
public
String
loginForm
(
Map
<
String
,
Object
>
map
)
{
final
FormClient
formClient
=
(
FormClient
)
config
.
getClients
().
findClient
(
Constants
.
LOCAL_CLIENT_NAME
).
get
();
map
.
put
(
"callbackUrl"
,
formClient
.
getCallbackUrl
());
...
...
@@ -76,8 +79,13 @@ public class SampleController {
return
null
;
}
@GetMapping
(
value
=
"/metadata"
,
produces
=
MediaType
.
APPLICATION_XML_VALUE
)
public
@ResponseBody
String
getMetadata
(
@RequestParam
(
required
=
false
)
boolean
generate
,
@RequestParam
(
required
=
false
)
boolean
filesystem
)
throws
Exception
{
@GetMapping
(
value
=
{
"/metadata"
,
"/metadata/{action}"
},
produces
=
MediaType
.
APPLICATION_XML_VALUE
)
public
@ResponseBody
String
getMetadata
(
@PathVariable
(
required
=
false
)
String
action
)
{
if
(
action
!=
null
&&
!
action
.
isEmpty
()
&&
!
action
.
equals
(
"generate"
)
&&
!
action
.
equals
(
"filesystem"
))
{
throw
new
NotFoundException
();
}
boolean
generate
=
action
!=
null
&&
action
.
equals
(
"generate"
);
boolean
filesystem
=
action
!=
null
&&
action
.
equals
(
"filesystem"
);
String
metadata
;
if
((
metadataHelper
.
isFilesystemMetadataAvailable
()
&&
!
generate
)
||
filesystem
)
{
metadata
=
metadataHelper
.
getFromFilesystem
();
...
...
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/error/NotFoundException.java
0 → 100644
View file @
5bd2d4f4
package
eu.dariah.de.dariahsp.sample.error
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.web.bind.annotation.ResponseStatus
;
@ResponseStatus
(
value
=
HttpStatus
.
NOT_FOUND
,
reason
=
"Not found"
)
public
class
NotFoundException
extends
RuntimeException
{
private
static
final
long
serialVersionUID
=
2297069740605303012L
;
}
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/error/SAML2MetadataNotFoundException.java
View file @
5bd2d4f4
...
...
@@ -3,7 +3,7 @@ package eu.dariah.de.dariahsp.sample.error;
import
org.springframework.http.HttpStatus
;
import
org.springframework.web.bind.annotation.ResponseStatus
;
@ResponseStatus
(
value
=
HttpStatus
.
NO
_CONTENT
,
reason
=
"SAML2 metadata not available"
)
@ResponseStatus
(
value
=
HttpStatus
.
NO
T_FOUND
,
reason
=
"SAML2 metadata not available"
)
public
class
SAML2MetadataNotFoundException
extends
RuntimeException
{
private
static
final
long
serialVersionUID
=
4211017703190145692L
;
}
dariahsp-sample/src/main/java/eu/dariah/de/dariahsp/sample/error/SecurityConfigurationException.java
0 → 100644
View file @
5bd2d4f4
package
eu.dariah.de.dariahsp.sample.error
;
public
class
SecurityConfigurationException
extends
Exception
{
private
static
final
long
serialVersionUID
=
-
7982246481903633882L
;
public
SecurityConfigurationException
()
{
super
();
}
public
SecurityConfigurationException
(
String
message
)
{
super
(
message
);
}
public
SecurityConfigurationException
(
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
}
public
SecurityConfigurationException
(
Throwable
cause
)
{
super
(
cause
);
}
protected
SecurityConfigurationException
(
String
message
,
Throwable
cause
,
boolean
enableSuppression
,
boolean
writableStackTrace
)
{
super
(
message
,
cause
,
enableSuppression
,
writableStackTrace
);
}
}
dariahsp-sample/src/main/resources/application.yml
View file @
5bd2d4f4
...
...
@@ -20,6 +20,7 @@ auth:
passhash
:
'
$2a$10$EeajSQQUepa7H7.g4xQCaeO.hjUwh0yzYCMrfOkWCZGe1IiWaexa6'
roles
:
[
"
ROLE_CONTRIBUTOR"
]
saml
:
enabled
:
true
keystore
:
path
:
/data/_srv/dariahsp/c105-229.cloud.gwdg.de.jks
pass
:
clariah
...
...
@@ -28,13 +29,14 @@ auth:
metadata
:
url
:
https://aaiproxy.de.dariah.eu/idp/
sp
:
#
externalMetadata: /etc/dfa/dme
/sp_metadata.xml
#
metadataResource: /data/_srv/dariahsp
/sp_metadata.xml
maxAuthAge
:
-1
entityId
:
https://c105-229.cloud.gwdg.de/dme
#baseUrl: https://c105-229.cloud.gwdg.de/dme
entityId
:
${auth.saml.sp.baseUrl}
signMetadata
:
true
#signingMethods: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
#digestMethods: http://www.w3.org/2001/04/xmlenc#sha256, http://www.w3.org/2001/04/xmlenc#sha512
#supportedProtocols: urn:oasis:names:tc:SAML:2.0:
assertion
#supportedProtocols: urn:oasis:names:tc:SAML:2.0:
protocol
authnRequestSigned
:
true
logoutRequestSigned
:
true
wantsAssertionsSigned
:
true
...
...
dariahsp-sample/src/main/resources/logback.xml
View file @
5bd2d4f4
...
...
@@ -13,9 +13,11 @@
<appender-ref
ref=
"Console"
/>
</root>
<!-- LOG "com.baeldung*" at TRACE level -->
<logger
name=
"eu.dariah.de"
level=
"trace"
additivity=
"false"
>
<appender-ref
ref=
"Console"
/>
</logger>
<logger
name=
"eu.dariah.de"
>
<level
value=
"debug"
/>
</logger>
<logger
name=
"org.pac4j"
>
<level
value=
"debug"
/>
</logger>
</configuration>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment