Skip to content

Commit 302d238

Browse files
committed
Add SSH-key credentials
1 parent e764646 commit 302d238

File tree

6 files changed

+161
-4
lines changed

6 files changed

+161
-4
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20250312233323 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return '';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
$this->addSql('ALTER TABLE credentials ADD key TEXT DEFAULT NULL');
20+
}
21+
22+
public function down(Schema $schema): void
23+
{
24+
$this->addSql('ALTER TABLE credentials DROP key');
25+
}
26+
}

src/Composer/ComposerClient.php

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,37 @@
22

33
namespace CodedMonkey\Dirigent\Composer;
44

5+
use CodedMonkey\Dirigent\Doctrine\Entity\CredentialsType;
56
use CodedMonkey\Dirigent\Doctrine\Entity\Package;
67
use CodedMonkey\Dirigent\Doctrine\Entity\Registry;
78
use Composer\Config;
89
use Composer\Factory;
910
use Composer\IO\IOInterface;
1011
use Composer\IO\NullIO;
12+
use Composer\Pcre\Preg;
1113
use Composer\Repository\ComposerRepository;
1214
use Composer\Repository\VcsRepository;
1315
use Composer\Util\HttpDownloader;
16+
use Composer\Util\Url;
17+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
18+
use Symfony\Component\Filesystem\Filesystem;
1419

15-
class ComposerClient
20+
// use Composer\Util\Filesystem as ComposerFilesystem;
21+
// use Composer\Util\Git as GitUtility;
22+
// use Composer\Util\ProcessExecutor;
23+
// use Symfony\Component\Process\Process;
24+
25+
readonly class ComposerClient
1626
{
27+
private Filesystem $filesystem;
28+
29+
public function __construct(
30+
#[Autowire(param: 'dirigent.storage.path')]
31+
private string $storagePath,
32+
) {
33+
$this->filesystem = new Filesystem();
34+
}
35+
1736
public function createComposerRepository(Package|Registry $registry, ?IOInterface $io = null, ?Config $config = null): ComposerRepository
1837
{
1938
$registry = $registry instanceof Package ? $registry->getMirrorRegistry() : $registry;
@@ -30,16 +49,24 @@ public function createComposerRepository(Package|Registry $registry, ?IOInterfac
3049

3150
public function createVcsRepository(Package $package, ?IOInterface $io = null, ?Config $config = null): VcsRepository
3251
{
33-
$repoUrl = $package->getRepositoryUrl();
52+
$repositoryUrl = $package->getRepositoryUrl();
53+
$repositoryCredentials = $package->getRepositoryCredentials();
3454

35-
$config ??= ConfigFactory::createForVcsRepository($repoUrl, $package->getRepositoryCredentials());
55+
$config ??= ConfigFactory::createForVcsRepository($repositoryUrl, $repositoryCredentials);
3656
if (!$io) {
3757
$io = new NullIO();
3858
$io->loadConfiguration($config);
3959
}
4060
$httpDownloader = $this->createHttpDownloader($io, $config);
4161

42-
return new VcsRepository(['url' => $repoUrl], $io, $config, $httpDownloader);
62+
$cacheRepositoryName = Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($repositoryUrl));
63+
$cachePath = $config->get('cache-vcs-dir') . '/' . $cacheRepositoryName . '/';
64+
65+
if (CredentialsType::SshKey === $repositoryCredentials->getType() && !$this->filesystem->exists($cachePath)) {
66+
$this->cloneAuthenticatedVcsRepository($package, $cachePath);
67+
}
68+
69+
return new VcsRepository(['url' => $repositoryUrl], $io, $config, $httpDownloader);
4370
}
4471

4572
public function createHttpDownloader(?IOInterface $io = null, ?Config $config = null): HttpDownloader
@@ -60,4 +87,80 @@ public static function getHttpDownloaderOptions(): array
6087

6188
return $options;
6289
}
90+
91+
private function cloneAuthenticatedVcsRepository(Package $package, string $cachePath): void
92+
{
93+
$repositoryUrl = $package->getRepositoryUrl();
94+
$repositoryCredentials = $package->getRepositoryCredentials();
95+
96+
$cacheRepositoryName = Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($repositoryUrl));
97+
$keyPath = "$this->storagePath/keys/$cacheRepositoryName";
98+
99+
// todo delete key file after every use
100+
$this->filesystem->mkdir(dirname($keyPath));
101+
$this->filesystem->dumpFile($keyPath, str_replace("\r", '', $repositoryCredentials->getKey() . PHP_EOL));
102+
$this->filesystem->chmod($keyPath, 0400);
103+
104+
$nullStream = fopen('/dev/null', 'c');
105+
$descriptorSpec = [
106+
['pipe', 'r'],
107+
$nullStream,
108+
$nullStream,
109+
];
110+
111+
$gitConfig = sprintf('core.sshcommand="ssh -i %s"', $keyPath);
112+
// todo improve error handling
113+
proc_open("git clone -c $gitConfig --mirror -- $repositoryUrl $cachePath", $descriptorSpec, $pipes);
114+
115+
/*
116+
// Concepts to clone with libraries
117+
118+
$gitUtility = new GitUtility(
119+
$io,
120+
$config,
121+
$process = new ProcessExecutor($io),
122+
new ComposerFilesystem($process),
123+
);
124+
125+
// The following commands give the following output:
126+
//
127+
// ssh -i <key-file>: No such file or directory
128+
// fatal: Could not read from remote repository.
129+
//
130+
// This is caused by Symfony Process enclosing each argument with ' which messes with
131+
// git using the proper ssh command (I guess).
132+
133+
$gitUtility->runCommands([
134+
['git', 'clone', '-c', $gitConfig, '--mirror', '--', '%url%', $cachePath],
135+
], $repositoryUrl, $cachePath, true, $lol);
136+
137+
$process = new Process(['git', 'clone', '-c', $gitConfig, '--mirror', $repositoryUrl, $cachePath]);
138+
$process->mustRun();
139+
140+
// The following code is an attempt to clone by initializing an empty
141+
// repository and adding the ssh command and remote afterward. Unfortunately
142+
// this doesn't mirror all branches like `git clone --mirror` does, which
143+
// Composer doesn't handle correctly.
144+
145+
$mirrorCachePath = $config->get('cache-vcs-dir') . '/' . $cacheRepositoryName . '~mirror/';
146+
$this->filesystem->mkdir($mirrorCachePath);
147+
148+
$gitUtility->runCommands([
149+
['git', 'init'],
150+
['git', 'config', 'core.sshCommand', "ssh -i $keyPath"],
151+
['git', 'remote', 'add', 'origin', '--', '%url%'],
152+
['git', 'remote', 'update', '--prune', 'origin'],
153+
['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'],
154+
['git', 'gc', '--auto'],
155+
], $repositoryUrl, $mirrorCachePath);
156+
157+
$this->filesystem->rename("$mirrorCachePath/.git", $cachePath);
158+
159+
$gitUtility->runCommands([
160+
['git', 'config', 'core.bare', 'true'],
161+
], $repositoryUrl, $cachePath);
162+
163+
$this->filesystem->remove($mirrorCachePath);
164+
*/
165+
}
63166
}

src/Controller/Dashboard/DashboardCredentialsController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,8 @@ public function configureFields(string $pageName): iterable
4747
yield TextField::new('token')
4848
->setFormTypeOption('row_attr', ['data-credentials-field' => 'token'])
4949
->onlyOnForms();
50+
yield TextareaField::new('key')
51+
->setFormTypeOption('row_attr', ['data-credentials-field' => 'key'])
52+
->onlyOnForms();
5053
}
5154
}

src/Doctrine/Entity/Credentials.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class Credentials
3232
#[ORM\Column(nullable: true)]
3333
private ?string $token = null;
3434

35+
#[ORM\Column(type: Types::TEXT, nullable: true)]
36+
private ?string $key = null;
37+
3538
public function getId(): ?int
3639
{
3740
return $this->id;
@@ -101,4 +104,14 @@ public function setToken(?string $token): void
101104
{
102105
$this->token = $token;
103106
}
107+
108+
public function getKey(): ?string
109+
{
110+
return $this->key;
111+
}
112+
113+
public function setKey(?string $key): void
114+
{
115+
$this->key = $key;
116+
}
104117
}

src/Doctrine/Entity/CredentialsType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ enum CredentialsType: string
88
case GithubOauthToken = 'github-oauth';
99
case GitlabDeployToken = 'gitlab-dt';
1010
case GitlabPersonalAccessToken = 'gitlab-pat';
11+
case SshKey = 'ssh-key';
1112
}

templates/dashboard/credentials/js_assets.html.twig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const usernameFieldRow = document.querySelector('[data-credentials-field="username"]');
44
const passwordFieldRow = document.querySelector('[data-credentials-field="password"]');
55
const tokenFieldRow = document.querySelector('[data-credentials-field="token"]');
6+
const keyFieldRow = document.querySelector('[data-credentials-field="key"]');
67
78
const initalType = document.querySelector('[name="Credentials[type]"]:checked')?.value ?? null;
89
if (initalType) {
@@ -20,22 +21,32 @@
2021
usernameFieldRow.style.display = null;
2122
passwordFieldRow.style.display = null;
2223
tokenFieldRow.style.display = 'none';
24+
keyFieldRow.style.display = 'none';
2325
} else if (type === 'github-oauth') {
2426
usernameFieldRow.style.display = 'none';
2527
passwordFieldRow.style.display = 'none';
2628
tokenFieldRow.style.display = null;
29+
keyFieldRow.style.display = 'none';
2730
} else if (type === 'gitlab-dt') {
2831
usernameFieldRow.style.display = null;
2932
passwordFieldRow.style.display = 'none';
3033
tokenFieldRow.style.display = null;
34+
keyFieldRow.style.display = 'none';
3135
} else if (type === 'gitlab-pat') {
3236
usernameFieldRow.style.display = 'none';
3337
passwordFieldRow.style.display = 'none';
3438
tokenFieldRow.style.display = null;
39+
keyFieldRow.style.display = 'none';
40+
} else if (type === 'ssh-key') {
41+
usernameFieldRow.style.display = 'none';
42+
passwordFieldRow.style.display = 'none';
43+
tokenFieldRow.style.display = 'none';
44+
keyFieldRow.style.display = null;
3545
} else {
3646
usernameFieldRow.style.display = null;
3747
passwordFieldRow.style.display = null;
3848
tokenFieldRow.style.display = null;
49+
keyFieldRow.style.display = null;
3950
}
4051
}
4152
}

0 commit comments

Comments
 (0)