@@ -18,6 +18,7 @@ import {
18
18
} from 'vault/components/recovery/page/snapshots/snapshot-utils' ;
19
19
20
20
import type ApiService from 'vault/services/api' ;
21
+ import type AuthService from 'vault/vault/services/auth' ;
21
22
import type NamespaceService from 'vault/services/namespace' ;
22
23
import type { SnapshotManageModel } from 'vault/routes/vault/cluster/recovery/snapshots/snapshot/manage' ;
23
24
@@ -29,12 +30,15 @@ type SecretData = { [key: string]: unknown };
29
30
30
31
type RecoveryData = {
31
32
models : string [ ] ;
32
- query ?: { namespace : string } ;
33
+ query ?: { [ key : string ] : string } ;
33
34
} ;
34
35
35
36
type MountOption = { type : RecoverySupportedEngines ; path : string } ;
37
+ type GroupedOption = { groupName : string ; options : MountOption [ ] } ;
36
38
37
39
export default class SnapshotManage extends Component < Args > {
40
+ // TODO remove once endpoint is updated to accepted `read_snapshot_id`
41
+ @service declare readonly auth : AuthService ;
38
42
@service declare readonly api : ApiService ;
39
43
@service declare readonly currentCluster : any ;
40
44
@service declare readonly namespace : NamespaceService ;
@@ -43,7 +47,7 @@ export default class SnapshotManage extends Component<Args> {
43
47
@tracked selectedMount ?: MountOption ;
44
48
@tracked resourcePath = '' ;
45
49
46
- @tracked mountOptions : MountOption [ ] = [ ] ;
50
+ @tracked mountOptions : GroupedOption [ ] = [ ] ;
47
51
@tracked secretData : SecretData | undefined ;
48
52
49
53
@tracked mountError = '' ;
@@ -59,6 +63,7 @@ export default class SnapshotManage extends Component<Args> {
59
63
private pollingController : { start : ( ) => Promise < void > ; cancel : ( ) => void } | null = null ;
60
64
61
65
recoverySupportedEngines = [
66
+ { display : 'Database' , value : SupportedSecretBackendsEnum . DATABASE } ,
62
67
{ display : 'Cubbyhole' , value : SupportedSecretBackendsEnum . CUBBYHOLE } ,
63
68
{ display : 'KV v1' , value : SupportedSecretBackendsEnum . KV } ,
64
69
] ;
@@ -142,7 +147,7 @@ export default class SnapshotManage extends Component<Args> {
142
147
const headers = this . api . buildHeaders ( { namespace } ) ;
143
148
const { secret } = await this . api . sys . internalUiListEnabledVisibleMounts ( headers ) ;
144
149
145
- this . mountOptions = this . api . responseObjectToArray ( secret , 'path' ) . flatMap ( ( engine ) => {
150
+ const mounts = this . api . responseObjectToArray ( secret , 'path' ) . flatMap ( ( engine ) => {
146
151
const eng = new SecretsEngineResource ( engine ) ;
147
152
148
153
// performance secondaries cannot perform snapshot operations on shared paths
@@ -159,6 +164,16 @@ export default class SnapshotManage extends Component<Args> {
159
164
]
160
165
: [ ] ;
161
166
} ) ;
167
+
168
+ const databases : MountOption [ ] = mounts . filter ( ( m ) => m . type === SupportedSecretBackendsEnum . DATABASE ) ;
169
+ const secretEngines : MountOption [ ] = mounts . filter (
170
+ ( m ) => m . type !== SupportedSecretBackendsEnum . DATABASE
171
+ ) ;
172
+
173
+ this . mountOptions = [
174
+ ...( databases . length ? [ { groupName : 'Databases' , options : databases } ] : [ ] ) ,
175
+ { groupName : 'Secret Engines' , options : secretEngines } ,
176
+ ] ;
162
177
} catch {
163
178
this . mountOptions = [ ] ;
164
179
}
@@ -218,6 +233,7 @@ export default class SnapshotManage extends Component<Args> {
218
233
const mountPath = this . selectedMount ?. path as string ;
219
234
const namespace = this . selectedNamespace === 'root' ? ROOT_NAMESPACE : this . selectedNamespace ;
220
235
const headers = this . api . buildHeaders ( { namespace } ) ;
236
+
221
237
switch ( mountType ) {
222
238
case SupportedSecretBackendsEnum . KV : {
223
239
const { data } = await this . api . secrets . kvV1Read (
@@ -234,6 +250,25 @@ export default class SnapshotManage extends Component<Args> {
234
250
this . secretData = data as SecretData ;
235
251
break ;
236
252
}
253
+ case SupportedSecretBackendsEnum . DATABASE : {
254
+ // TODO remove once endpoint is updated to accept `read_snapshot_id`
255
+ const { currentToken } = this . auth ;
256
+
257
+ const resp = await fetch (
258
+ `/v1/${ mountPath } /static-roles/${ this . resourcePath } ?read_snapshot_id=${ snapshot_id } ` ,
259
+ {
260
+ method : 'GET' ,
261
+ headers : {
262
+ 'X-Vault-Namespace' : namespace ,
263
+ 'X-Vault-Token' : currentToken ,
264
+ } ,
265
+ }
266
+ ) ;
267
+
268
+ const { data } = await resp . json ( ) ;
269
+ this . secretData = data as SecretData ;
270
+ break ;
271
+ }
237
272
default : {
238
273
// This should never be reached, but just in case
239
274
throw new Error ( 'Unsupported recovery engine' ) ;
@@ -258,30 +293,46 @@ export default class SnapshotManage extends Component<Args> {
258
293
const { snapshot_id } = this . args . model . snapshot as { snapshot_id : string } ;
259
294
const mountType = this . selectedMount ?. type ;
260
295
const mountPath = this . selectedMount ?. path as string ;
296
+
261
297
const namespace = this . selectedNamespace === 'root' ? ROOT_NAMESPACE : this . selectedNamespace ;
262
- const headers = this . api . buildHeaders ( { namespace } ) ;
298
+ const headers = this . api . buildHeaders ( { namespace, recoverSnapshotId : snapshot_id } ) ;
299
+
300
+ // this query is used to build the recovered resource link in the success message
301
+ let query : { [ key : string ] : string } = { } ;
302
+ if ( namespace && namespace !== this . namespace . path ) {
303
+ query = { namespace } ;
304
+ }
305
+
306
+ // Certain backends have a prefix which is needed for the recovery link we show to the user
307
+ let modelPrefix = '' ;
263
308
switch ( mountType ) {
264
309
case SupportedSecretBackendsEnum . KV : {
265
310
await this . api . secrets . kvV1Write ( this . resourcePath , mountPath , { } , snapshot_id , undefined , headers ) ;
266
311
break ;
267
312
}
268
313
case SupportedSecretBackendsEnum . CUBBYHOLE : {
269
- this . api . buildHeaders ( { namespace : namespace || this . namespace . path } ) ;
270
314
await this . api . secrets . cubbyholeWrite ( this . resourcePath , { } , snapshot_id , undefined , headers ) ;
271
315
break ;
272
316
}
317
+ case SupportedSecretBackendsEnum . DATABASE : {
318
+ await this . api . secrets . databaseWriteStaticRole ( this . resourcePath , mountPath , { } , headers ) ;
319
+ modelPrefix = 'role/' ;
320
+ query = {
321
+ ...query ,
322
+ type : 'static' ,
323
+ } ;
324
+ break ;
325
+ }
273
326
default : {
274
327
// This should never be reached, but just in case
275
328
throw new Error ( 'Unsupported recovery engine' ) ;
276
329
}
277
330
}
278
331
279
332
this . recoveryData = {
280
- models : [ mountPath , this . resourcePath ] ,
333
+ models : [ mountPath , modelPrefix + this . resourcePath ] ,
334
+ query,
281
335
} ;
282
- if ( namespace && namespace !== this . namespace . path ) {
283
- this . recoveryData . query = { namespace } ;
284
- }
285
336
} catch ( e ) {
286
337
const error = await this . api . parseError ( e ) ;
287
338
this . bannerError = `Snapshot recovery error: ${ error . message } ` ;
0 commit comments