Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
hhit
config-generator
Commits
56ebc243
Commit
56ebc243
authored
Mar 06, 2021
by
Hendrik Heneke
Browse files
Added commands for secrets management.
parent
ed373eda
Pipeline
#376
passed with stage
in 37 seconds
Changes
10
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
440 additions
and
24 deletions
+440
-24
src/Command/AbstractCommand.php
src/Command/AbstractCommand.php
+34
-0
src/Command/DumpPrivateKeyCommand.php
src/Command/DumpPrivateKeyCommand.php
+19
-0
src/Command/GenerateConfigsCommand.php
src/Command/GenerateConfigsCommand.php
+4
-17
src/Command/GenerateKeysCommand.php
src/Command/GenerateKeysCommand.php
+87
-0
src/Command/ListSecretsCommand.php
src/Command/ListSecretsCommand.php
+68
-0
src/Command/RemoveSecretsCommand.php
src/Command/RemoveSecretsCommand.php
+51
-0
src/Command/SavePrivateKeyCommand.php
src/Command/SavePrivateKeyCommand.php
+31
-0
src/Command/SetSecretsCommand.php
src/Command/SetSecretsCommand.php
+93
-0
src/Generator/Factory.php
src/Generator/Factory.php
+43
-7
src/cfgen.php
src/cfgen.php
+10
-0
No files found.
src/Command/AbstractCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
HHIT\ConfigGenerator\Generator\Factory
;
use
Symfony\Component\Console\Command\Command
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Input\InputOption
;
abstract
class
AbstractCommand
extends
Command
{
protected
string
$projectDir
;
public
function
__construct
(
string
$projectDir
,
?string
$name
=
null
)
{
$this
->
projectDir
=
$projectDir
;
parent
::
__construct
(
$name
);
}
protected
function
configure
()
{
$this
->
addOption
(
'project'
,
'p'
,
InputOption
::
VALUE_OPTIONAL
,
'project (dir)'
,
$this
->
projectDir
);
$this
->
addOption
(
'env'
,
'e'
,
InputOption
::
VALUE_OPTIONAL
,
'environment'
,
'dev'
);
}
protected
function
createFactory
(
InputInterface
$input
):
Factory
{
$env
=
$input
->
getOption
(
'env'
);
$factory
=
new
Factory
(
$input
->
hasOption
(
'project'
)
?
$input
->
getOption
(
'project'
)
:
$this
->
projectDir
,
$env
);
$factory
->
bootEnv
();
return
$factory
;
}
}
src/Command/DumpPrivateKeyCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Output\OutputInterface
;
class
DumpPrivateKeyCommand
extends
AbstractCommand
{
protected
static
$defaultName
=
'dump-private-key'
;
protected
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$factory
=
$this
->
createFactory
(
$input
);
$output
->
writeln
(
'<info>'
.
$factory
->
dumpPrivateKey
()
.
'</info>'
);
return
0
;
}
}
src/Command/GenerateConfigsCommand.php
View file @
56ebc243
...
...
@@ -9,35 +9,22 @@ use Symfony\Component\Console\Input\InputInterface;
use
Symfony\Component\Console\Input\InputOption
;
use
Symfony\Component\Console\Output\OutputInterface
;
class
GenerateConfigsCommand
extends
Command
class
GenerateConfigsCommand
extends
Abstract
Command
{
protected
static
$defaultName
=
"generate-configs"
;
private
string
$projectDir
;
public
function
__construct
(
string
$projectDir
,
?string
$name
=
null
)
{
$this
->
projectDir
=
$projectDir
;
parent
::
__construct
(
$name
);
}
protected
function
configure
()
{
$this
->
addOption
(
'project'
,
'p'
,
InputOption
::
VALUE_OPTIONAL
,
'project (dir)'
,
$this
->
projectDir
);
$this
->
addOption
(
'env'
,
'e'
,
InputOption
::
VALUE_OPTIONAL
,
'environment'
,
'dev'
);
parent
::
configure
();
$this
->
addOption
(
'config'
,
'c'
,
InputOption
::
VALUE_OPTIONAL
,
'configuration file'
);
$this
->
addOption
(
'overwrite'
,
null
,
InputOption
::
VALUE_NONE
,
'overwrite existing files'
);
}
public
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$env
=
$input
->
getOption
(
'env'
);
$factory
=
new
Factory
(
$input
->
hasOption
(
'project'
)
?
$input
->
getOption
(
'project'
)
:
$this
->
projectDir
,
$env
);
$factory
->
bootEnv
();
$factory
=
$this
->
createFactory
(
$input
);
$generator
=
$factory
->
createGenerator
();
$output
->
writeln
(
"<info>Generating configuration files
for
{
$env
}
</info>"
);
$output
->
writeln
(
"<info>Generating configuration files</info>"
);
$success
=
$generator
->
processConfigurations
(
'symfony'
,
$input
->
getOption
(
'overwrite'
)
?
true
:
false
,
...
...
src/Command/GenerateKeysCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Input\InputOption
;
use
Symfony\Component\Console\Output\ConsoleOutputInterface
;
use
Symfony\Component\Console\Output\OutputInterface
;
use
Symfony\Component\Console\Style\SymfonyStyle
;
class
GenerateKeysCommand
extends
AbstractCommand
{
protected
static
$defaultName
=
'generate-keys'
;
protected
function
configure
()
{
parent
::
configure
();
$this
->
addOption
(
'local'
,
'l'
,
InputOption
::
VALUE_NONE
,
'Updates the local vault.'
);
$this
->
addOption
(
'rotate'
,
'r'
,
InputOption
::
VALUE_NONE
,
'Re-encrypts existing secrets with the newly generated keys.'
);
}
public
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$factory
=
$this
->
createFactory
(
$input
);
$vault
=
$factory
->
createSodiumVault
();
$localVault
=
$factory
->
createDotenvVault
();
$io
=
new
SymfonyStyle
(
$input
,
$output
instanceof
ConsoleOutputInterface
?
$output
->
getErrorOutput
()
:
$output
);
$vaultToUse
=
$input
->
getOption
(
'local'
)
?
$localVault
:
$vault
;
if
(
null
===
$vaultToUse
)
{
$io
->
success
(
'The local vault is disabled.'
);
return
1
;
}
if
(
!
$input
->
getOption
(
'rotate'
))
{
if
(
$vaultToUse
->
generateKeys
())
{
$io
->
success
(
$vaultToUse
->
getLastMessage
());
if
(
$vault
===
$vaultToUse
)
{
$io
->
caution
(
'DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️'
);
}
return
0
;
}
$io
->
warning
(
$vaultToUse
->
getLastMessage
());
return
1
;
}
$secrets
=
[];
foreach
(
$vaultToUse
->
list
(
true
)
as
$name
=>
$value
)
{
if
(
null
===
$value
)
{
$io
->
error
(
$vaultToUse
->
getLastMessage
());
return
1
;
}
$secrets
[
$name
]
=
$value
;
}
if
(
!
$vaultToUse
->
generateKeys
(
true
))
{
$io
->
warning
(
$vaultToUse
->
getLastMessage
());
return
1
;
}
$io
->
success
(
$vaultToUse
->
getLastMessage
());
if
(
$secrets
)
{
foreach
(
$secrets
as
$name
=>
$value
)
{
$vaultToUse
->
seal
(
$name
,
$value
);
}
$io
->
comment
(
'Existing secrets have been rotated to the new keys.'
);
}
if
(
$vault
===
$vaultToUse
)
{
$io
->
caution
(
'DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️'
);
}
return
0
;
}
}
src/Command/ListSecretsCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
Symfony\Component\Console\Helper\Dumper
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Input\InputOption
;
use
Symfony\Component\Console\Output\ConsoleOutputInterface
;
use
Symfony\Component\Console\Output\OutputInterface
;
use
Symfony\Component\Console\Style\SymfonyStyle
;
class
ListSecretsCommand
extends
AbstractCommand
{
protected
static
$defaultName
=
"list-secrets"
;
protected
function
configure
()
{
parent
::
configure
();
$this
->
addOption
(
'reveal'
,
'r'
,
InputOption
::
VALUE_NONE
,
'Display decrypted values alongside names'
);
}
public
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$factory
=
$this
->
createFactory
(
$input
);
$vault
=
$factory
->
createSodiumVault
();
$localVault
=
$factory
->
createDotenvVault
();
$io
=
new
SymfonyStyle
(
$input
,
$output
instanceof
ConsoleOutputInterface
?
$output
->
getErrorOutput
()
:
$output
);
if
(
!
$reveal
=
$input
->
getOption
(
'reveal'
))
{
$io
->
comment
(
sprintf
(
'To reveal the secrets run <info>php %s %s --reveal</info>'
,
$_SERVER
[
'PHP_SELF'
],
$this
->
getName
()));
}
$secrets
=
$vault
->
list
(
$reveal
);
$localSecrets
=
null
!==
$localVault
?
$localVault
->
list
(
$reveal
)
:
null
;
$rows
=
[];
$dump
=
new
Dumper
(
$output
);
$dump
=
static
function
(
?string
$v
)
use
(
$dump
)
{
return
null
===
$v
?
'******'
:
$dump
(
$v
);
};
foreach
(
$secrets
as
$name
=>
$value
)
{
$rows
[
$name
]
=
[
$name
,
$dump
(
$value
)];
}
if
(
null
!==
$message
=
$vault
->
getLastMessage
())
{
$io
->
comment
(
$message
);
}
foreach
(
$localSecrets
??
[]
as
$name
=>
$value
)
{
if
(
isset
(
$rows
[
$name
]))
{
$rows
[
$name
][]
=
$dump
(
$value
);
}
}
if
(
null
!==
$localVault
&&
null
!==
$message
=
$localVault
->
getLastMessage
())
{
$io
->
comment
(
$message
);
}
(
new
SymfonyStyle
(
$input
,
$output
))
->
table
([
'Secret'
,
'Value'
]
+
(
null
!==
$localSecrets
?
[
2
=>
'Local Value'
]
:
[]),
$rows
);
return
0
;
}
}
src/Command/RemoveSecretsCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
Symfony\Component\Console\Input\InputArgument
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Input\InputOption
;
use
Symfony\Component\Console\Output\ConsoleOutputInterface
;
use
Symfony\Component\Console\Output\OutputInterface
;
use
Symfony\Component\Console\Style\SymfonyStyle
;
class
RemoveSecretsCommand
extends
AbstractCommand
{
protected
static
$defaultName
=
'remove-secrets'
;
protected
function
configure
()
{
parent
::
configure
();
$this
->
addArgument
(
'name'
,
InputArgument
::
REQUIRED
,
'The name of the secret'
);
$this
->
addOption
(
'local'
,
'l'
,
InputOption
::
VALUE_NONE
,
'Updates the local vault.'
);
}
public
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$factory
=
$this
->
createFactory
(
$input
);
$vault
=
$factory
->
createSodiumVault
();
$localVault
=
$factory
->
createDotenvVault
();
$io
=
new
SymfonyStyle
(
$input
,
$output
instanceof
ConsoleOutputInterface
?
$output
->
getErrorOutput
()
:
$output
);
$vault
=
$input
->
getOption
(
'local'
)
?
$localVault
:
$vault
;
if
(
null
===
$vault
)
{
$io
->
success
(
'The local vault is disabled.'
);
return
1
;
}
if
(
$vault
->
remove
(
$name
=
$input
->
getArgument
(
'name'
)))
{
$io
->
success
(
$vault
->
getLastMessage
()
??
'Secret was removed from the vault.'
);
}
else
{
$io
->
comment
(
$vault
->
getLastMessage
()
??
'Secret was not found in the vault.'
);
}
if
(
$vault
===
$vault
&&
null
!==
$localVault
->
reveal
(
$name
))
{
$io
->
comment
(
'Note that this secret is overridden in the local vault.'
);
}
return
0
;
}
}
src/Command/SavePrivateKeyCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Input\InputOption
;
use
Symfony\Component\Console\Output\OutputInterface
;
class
SavePrivateKeyCommand
extends
AbstractCommand
{
protected
static
$defaultName
=
'save-private-key'
;
protected
function
configure
()
{
parent
::
configure
();
$this
->
addOption
(
'key'
,
'k'
,
InputOption
::
VALUE_REQUIRED
,
'private key'
);
}
protected
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$factory
=
$this
->
createFactory
(
$input
);
$key
=
$input
->
getOption
(
'key'
);
if
(
!
$key
)
{
throw
new
\
RuntimeException
(
'Key required!'
);
}
$factory
->
savePrivateKey
(
$key
);
$output
->
writeln
(
'<info>private key saved</info>'
);
return
0
;
}
}
src/Command/SetSecretsCommand.php
0 → 100644
View file @
56ebc243
<?php
declare
(
strict_types
=
1
);
namespace
HHIT\ConfigGenerator\Command
;
use
Symfony\Component\Console\Input\InputArgument
;
use
Symfony\Component\Console\Input\InputInterface
;
use
Symfony\Component\Console\Input\InputOption
;
use
Symfony\Component\Console\Output\ConsoleOutputInterface
;
use
Symfony\Component\Console\Output\OutputInterface
;
use
Symfony\Component\Console\Style\SymfonyStyle
;
class
SetSecretsCommand
extends
AbstractCommand
{
protected
static
$defaultName
=
"set-secrets"
;
protected
function
configure
()
{
parent
::
configure
();
$this
->
addArgument
(
'name'
,
InputArgument
::
REQUIRED
,
'The name of the secret'
);
$this
->
addArgument
(
'file'
,
InputArgument
::
OPTIONAL
,
'A file where to read the secret from or "-" for reading from STDIN'
);
$this
->
addOption
(
'local'
,
'l'
,
InputOption
::
VALUE_NONE
,
'Updates the local vault.'
);
$this
->
addOption
(
'random'
,
'r'
,
InputOption
::
VALUE_OPTIONAL
,
'Generates a random value.'
,
false
);
}
public
function
execute
(
InputInterface
$input
,
OutputInterface
$output
)
{
$factory
=
$this
->
createFactory
(
$input
);
$vault
=
$factory
->
createSodiumVault
();
$localVault
=
$factory
->
createDotenvVault
();
$errOutput
=
$output
instanceof
ConsoleOutputInterface
?
$output
->
getErrorOutput
()
:
$output
;
$io
=
new
SymfonyStyle
(
$input
,
$errOutput
);
$name
=
$input
->
getArgument
(
'name'
);
$vaultToUse
=
$input
->
getOption
(
'local'
)
?
$localVault
:
$vault
;
if
(
null
===
$vaultToUse
)
{
$io
->
error
(
'The local vault is disabled.'
);
return
1
;
}
if
(
$localVault
===
$vaultToUse
&&
!
\
array_key_exists
(
$name
,
$vault
->
list
()))
{
$io
->
error
(
sprintf
(
'Secret "%s" does not exist in the vault, you cannot override it locally.'
,
$name
));
return
1
;
}
if
(
0
<
$random
=
$input
->
getOption
(
'random'
)
??
16
)
{
$value
=
strtr
(
substr
(
base64_encode
(
random_bytes
(
$random
)),
0
,
$random
),
'+/'
,
'-_'
);
}
elseif
(
!
$file
=
$input
->
getArgument
(
'file'
))
{
$value
=
$io
->
askHidden
(
'Please type the secret value'
);
if
(
null
===
$value
)
{
$io
->
warning
(
'No value provided: using empty string'
);
$value
=
''
;
}
}
elseif
(
'-'
===
$file
)
{
$value
=
file_get_contents
(
'php://stdin'
);
}
elseif
(
is_file
(
$file
)
&&
is_readable
(
$file
))
{
$value
=
file_get_contents
(
$file
);
}
elseif
(
!
is_file
(
$file
))
{
throw
new
\
InvalidArgumentException
(
sprintf
(
'File not found: "%s".'
,
$file
));
}
elseif
(
!
is_readable
(
$file
))
{
throw
new
\
InvalidArgumentException
(
sprintf
(
'File is not readable: "%s".'
,
$file
));
}
if
(
$vaultToUse
->
generateKeys
())
{
$io
->
success
(
$vaultToUse
->
getLastMessage
());
if
(
$vaultToUse
===
$vault
)
{
$io
->
caution
(
'DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️'
);
}
}
$vaultToUse
->
seal
(
$name
,
$value
);
$io
->
success
(
$vaultToUse
->
getLastMessage
()
??
'Secret was successfully stored in the vault.'
);
if
(
0
<
$random
)
{
$errOutput
->
write
(
' // The generated random value is: <comment>'
);
$output
->
write
(
$value
);
$errOutput
->
writeln
(
'</comment>'
);
$io
->
newLine
();
}
if
(
$vaultToUse
===
$vault
&&
null
!==
$localVault
->
reveal
(
$name
))
{
$io
->
comment
(
'Note that this secret is overridden in the local vault.'
);
}
return
0
;
}
}
src/Generator/Factory.php
View file @
56ebc243
...
...
@@ -14,7 +14,6 @@ use Symfony\Component\Dotenv\Dotenv;
class
Factory
{
private
string
$projectDir
;
private
string
$env
;
...
...
@@ -29,7 +28,39 @@ class Factory
return
new
DefinitionReader
(
$this
->
projectDir
);
}
function
createSodiumVault
():
SodiumVault
public
function
dumpPrivateKey
()
{
$file
=
$this
->
projectDir
.
'/config/secrets/'
.
$this
->
env
.
'/'
.
$this
->
env
.
'.decrypt.private.php'
;
if
(
!
file_exists
(
$file
))
{
throw
new
\
RuntimeException
(
"Key file
{
$file
}
does not exist!"
);
}
$key
=
include
(
$file
);
return
base64_encode
(
rawurldecode
(
str_replace
(
'\x'
,
'%'
,
$key
)));
}
public
function
savePrivateKey
(
string
$key
)
{
$file
=
$this
->
projectDir
.
'/config/secrets/'
.
$this
->
env
.
'/'
.
$this
->
env
.
'.decrypt.private.php'
;
$dirname
=
dirname
(
$file
);
if
(
file_exists
(
$file
))
{
throw
new
\
RuntimeException
(
"Key file
{
$file
}
already exists!"
);
}
if
(
!
file_exists
(
$dirname
))
{
throw
new
\
RuntimeException
(
"Directory
{
$dirname
}
does not exist!"
);
}
$data
=
str_replace
(
'%'
,
'\x'
,
rawurlencode
(
base64_decode
(
$key
)));
$data
=
sprintf
(
"<?php // %s on %s
\n\n
return
\"
%s
\"
;
\n
"
,
basename
(
$file
),
date
(
'r'
),
$data
);
if
(
false
===
file_put_contents
(
$file
,
$data
,
\
LOCK_EX
))
{
$e
=
error_get_last
();
throw
new
\
ErrorException
(
$e
[
'message'
]
??
'Failed to write secrets data.'
,
0
,
$e
[
'type'
]
??
\
E_USER_WARNING
);
}
}
public
function
createSodiumVault
():
SodiumVault
{
return
$this
->
createSodiumVaultInternal
(
$this
->
projectDir
.
'/config/secrets/'
.
$this
->
env
);
}
...
...
@@ -39,16 +70,21 @@ class Factory
return
new
SodiumVault
(
$secretsDir
,
$decryptionKey
);
}
private
function
createDotenvVault
(
string
$dotenvFile
):
DotenvVault
public
function
createDotenvVault
():
DotenvVault
{
return
$this
->
createDotenvVaultInternal
();
}
private
function
createDotenvVaultInternal
():
DotenvVault
{
return
new
DotenvVault
(
$
dotenvFile
);
return
new
DotenvVault
(
$
this
->
projectDir
.
'/.env.'
.
$this
->
env
.
'.local'
);
}
private
function
createSymfonyVaultSecretProvider
():
SymfonyVaultSecretProvider
{
return
new
SymfonyVaultSecretProvider
(
$this
->
createSodiumVault
(),
$this
->
createDotenvVault
(
$this
->
projectDir
.
'/.env'
)
$this
->
createDotenvVault
()
);
}
...
...
@@ -67,7 +103,7 @@ class Factory
return
new
ValidatorFactory
();
}
function
createGenerator
():
Generator
public
function
createGenerator
():
Generator
{
return
new
Generator
(
$this
->
createDefinitionReader
(),
...
...
@@ -76,7 +112,7 @@ class Factory
);
}
function
bootEnv
()
public
function
bootEnv
()
{
if
(
is_array
(
$env
=
@
include
$this
->
projectDir
.
'/.env.local.php'
)
&&
(
!
isset
(
$env
[
'APP_ENV'
])
||
(
$_SERVER
[
'APP_ENV'
]
??
$_ENV
[
'APP_ENV'
]
??
$env
[
'APP_ENV'
])
===
$env
[
'APP_ENV'
]))
{
(
new
Dotenv
(
false
))
->
populate
(
$env
);
...
...
src/cfgen.php
View file @
56ebc243
<?php
use
HHIT\ConfigGenerator\Command\DumpPrivateKeyCommand
;
use
HHIT\ConfigGenerator\Command\GenerateConfigsCommand
;
use
HHIT\ConfigGenerator\Command\GenerateKeysCommand
;
use
HHIT\ConfigGenerator\Command\ListSecretsCommand
;
use
HHIT\ConfigGenerator\Command\SavePrivateKeyCommand
;
use
HHIT\ConfigGenerator\Command\SetSecretsCommand
;
use
Symfony\Component\Console\Application
;
use
Symfony\Component\Console\Input\ArgvInput
;
use
Symfony\Component\Console\Output\ConsoleOutput
;
...
...
@@ -16,6 +21,11 @@ unset($candidates, $autoloaderPathCandidate, $pojectDirCandidate);
$application
=
new
Application
(
'Configuration Generator'
);
$application
->
add
(
new
GenerateConfigsCommand
(
getcwd
()));
$application
->
add
(
new
ListSecretsCommand
(
getcwd
()));
$application
->
add
(
new
SetSecretsCommand
(
getcwd
()));
$application
->
add
(
new
GenerateKeysCommand
(
getcwd
()));
$application
->
add
(
new
DumpPrivateKeyCommand
(
getcwd
()));
$application
->
add
(
new
SavePrivateKeyCommand
(
getcwd
()));
$input
=
new
ArgvInput
();
if
(
null
!==
$env
=
$input
->
getParameterOption
([
'--env'
,
'-e'
],
'dev'
,
true
))
{
...
...
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