Passed
Push — master ( 3896ee...e50616 )
by Raffael
05:45
created

Factory::resolve()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 0
cts 9
cp 0
rs 9.7333
c 0
b 0
f 0
cc 3
nc 2
nop 2
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee.io
7
 *
8
 * @copyright   Copryright (c) 2017-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Tubee\Secret;
13
14
use Generator;
15
use MongoDB\BSON\ObjectIdInterface;
16
use MongoDB\Database;
17
use ParagonIE\Halite\KeyFactory;
18
use ParagonIE\Halite\Symmetric\Crypto as Symmetric;
19
use ParagonIE\Halite\Symmetric\EncryptionKey;
20
use ParagonIE\HiddenString\HiddenString;
21
use Psr\Log\LoggerInterface;
22
use Tubee\Helper;
23
use Tubee\Resource\Factory as ResourceFactory;
24
use Tubee\Resource\ResourceInterface;
25
use Tubee\ResourceNamespace\ResourceNamespaceInterface;
26
use Tubee\Secret;
27
28
class Factory extends ResourceFactory
29
{
30
    /**
31
     * Collection name.
32
     */
33
    public const COLLECTION_NAME = 'secrets';
34
35
    /**
36
     * Default key.
37
     */
38
    private const DEFAULT_KEY = '314004004b3cef33ba8ea540b424736408364317d9ebfbc9293b8478a8d2478e23dba1ba30ded48ab0dd059cfe3dce2daf00d10eb40af1c0bf429553a2d64802272a514cfde95ac31956baa3929ee01c7338c95805c3a619e254f7aa2966e6a7cdad4783';
39
40
    /**
41
     * Encryption key.
42
     *
43
     * @var EncryptionKey
44
     */
45
    protected $key;
46
47
    /**
48
     * Initialize.
49
     */
50
    public function __construct(Database $db, EncryptionKey $key, LoggerInterface $logger)
51
    {
52
        $this->key = $key;
53
        parent::__construct($db, $logger);
54
    }
55
56
    /**
57
     * Has secret.
58
     */
59
    public function has(ResourceNamespaceInterface $namespace, string $name): bool
60
    {
61
        return $this->db->{self::COLLECTION_NAME}->count([
62
            'namespace' => $namespace->getName(),
63
            'name' => $name,
64
        ]) > 0;
65
    }
66
67
    /**
68
     * Get all.
69
     */
70
    public function getAll(ResourceNamespaceInterface $namespace, ?array $query = null, ?int $offset = null, ?int $limit = null, ?array $sort = null): Generator
71
    {
72
        $filter = [
73
            'namespace' => $namespace->getName(),
74
        ];
75
76
        if (!empty($query)) {
77
            $filter = [
78
                '$and' => [$filter, $query],
79
            ];
80
        }
81
82
        return $this->getAllFrom($this->db->{self::COLLECTION_NAME}, $filter, $offset, $limit, $sort, function (array $resource) use ($namespace) {
83
            return $this->build($resource, $namespace);
84
        });
85
    }
86
87
    /**
88
     * Get secret.
89
     */
90
    public function getOne(ResourceNamespaceInterface $namespace, string $name): SecretInterface
91
    {
92
        $result = $this->db->{self::COLLECTION_NAME}->findOne([
93
            'namespace' => $namespace->getName(),
94
            'name' => $name,
95
        ], [
96
            'projection' => ['history' => 0],
97
        ]);
98
99
        if ($result === null) {
100
            throw new Exception\NotFound('secret '.$name.' is not registered');
101
        }
102
103
        return $this->build($result, $namespace);
104
    }
105
106
    /**
107
     * Resolve resource secrets.
108
     */
109
    public function resolve(ResourceNamespaceInterface $namespace, array $resource): array
110
    {
111
        if (isset($resource['secrets'])) {
112
            $this->logger->info('found secrets to resolve for resource ['.$resource['name'].']', [
113
                'category' => get_class($this),
114
            ]);
115
116
            foreach ($resource['secrets'] as $secret) {
117
                $blob = $this->getOne($namespace, $secret['secret'])->getData();
118
                $data = base64_decode(Helper::getArrayValue($blob, $secret['key']));
119
                $resource = Helper::setArrayValue($resource, $secret['to'], $data);
120
            }
121
        }
122
123
        return $resource;
124
    }
125
126
    /**
127
     * Reverse resolved secrets.
128
     */
129 2
    public static function reverse(ResourceInterface $resource, array $result): array
130
    {
131 2
        foreach ($resource->getSecrets() as $secret) {
132
            $result = Helper::deleteArrayValue($result, $secret['to']);
133
        }
134
135 2
        return $result;
136
    }
137
138
    /**
139
     * Delete by name.
140
     */
141
    public function deleteOne(ResourceNamespaceInterface $namespace, string $name): bool
142
    {
143
        $resource = $this->getOne($namespace, $name);
144
145
        return $this->deleteFrom($this->db->{self::COLLECTION_NAME}, $resource->getId());
146
    }
147
148
    /**
149
     * Update.
150
     */
151
    public function update(SecretInterface $resource, array $data): bool
152
    {
153
        $data['name'] = $resource->getName();
154
        $data['kind'] = $resource->getKind();
155
        $data = $this->validate($data);
156
        $data = $this->crypt($data);
157
158
        return $this->updateIn($this->db->{self::COLLECTION_NAME}, $resource, $data);
159
    }
160
161
    /**
162
     * Add secret.
163
     */
164
    public function add(ResourceNamespaceInterface $namespace, array $resource): ObjectIdInterface
165
    {
166
        $resource['kind'] = 'Secret';
167
        $resource = $this->validate($resource);
168
169
        if ($this->has($namespace, $resource['name'])) {
170
            throw new Exception\NotUnique('secret '.$resource['name'].' does already exists');
171
        }
172
173
        $resource = $this->crypt($resource);
174
175
        $resource['namespace'] = $namespace->getName();
176
177
        return $this->addTo($this->db->{self::COLLECTION_NAME}, $resource);
178
    }
179
180
    /**
181
     * Change stream.
182
     */
183
    public function watch(ResourceNamespaceInterface $namespace, ?ObjectIdInterface $after = null, bool $existing = true, ?array $query = null, ?int $offset = null, ?int $limit = null, ?array $sort = null): Generator
0 ignored issues
show
Unused Code introduced by
The parameter $namespace is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
184
    {
185
        return $this->watchFrom($this->db->{self::COLLECTION_NAME}, $after, $existing, $query, null, $offset, $limit, $sort);
186
    }
187
188
    /**
189
     * Build instance.
190
     */
191
    public function build(array $resource, ResourceNamespaceInterface $namespace): SecretInterface
192
    {
193
        $decrypted = json_decode(Symmetric::decrypt($resource['blob'], $this->key)->getString(), true);
194
        $resource['data'] = $decrypted;
195
        unset($resource['blob']);
196
197
        return $this->initResource(new Secret($resource, $namespace));
198
    }
199
200
    /**
201
     * Encrypt resource data.
202
     */
203
    protected function crypt(array $resource): array
204
    {
205
        if (KeyFactory::export($this->key)->getString() === self::DEFAULT_KEY) {
206
            throw new Exception\InvalidEncryptionKey('encryption key required to be changed');
207
        }
208
209
        $message = new HiddenString(json_encode($resource['data']));
210
        $resource['blob'] = Symmetric::encrypt($message, $this->key);
211
        unset($resource['data']);
212
213
        return $resource;
214
    }
215
}
216