@@ -113,6 +113,67 @@ public async Task<SecretVersionResponseModel> GetByIdAsync([FromRoute] Guid id)
113113 return new SecretVersionResponseModel ( secretVersion ) ;
114114 }
115115
116+ [ HttpPost ( "secret-versions/get-by-ids" ) ]
117+ public async Task < ListResponseModel < SecretVersionResponseModel > > GetManyByIdsAsync ( [ FromBody ] List < Guid > ids )
118+ {
119+ if ( ! ids . Any ( ) )
120+ {
121+ throw new BadRequestException ( "No version IDs provided." ) ;
122+ }
123+
124+ // Get all versions
125+ var versions = ( await _secretVersionRepository . GetManyByIdsAsync ( ids ) ) . ToList ( ) ;
126+ if ( ! versions . Any ( ) )
127+ {
128+ throw new NotFoundException ( ) ;
129+ }
130+
131+ // Get all associated secrets and check permissions
132+ var secretIds = versions . Select ( v => v . SecretId ) . Distinct ( ) . ToList ( ) ;
133+ var secrets = ( await _secretRepository . GetManyByIds ( secretIds ) ) . ToList ( ) ;
134+
135+ if ( ! secrets . Any ( ) )
136+ {
137+ throw new NotFoundException ( ) ;
138+ }
139+
140+ // Ensure all secrets belong to the same organization
141+ var organizationId = secrets . First ( ) . OrganizationId ;
142+ if ( secrets . Any ( s => s . OrganizationId != organizationId ) ||
143+ ! _currentContext . AccessSecretsManager ( organizationId ) )
144+ {
145+ throw new NotFoundException ( ) ;
146+ }
147+
148+ // For service accounts and organization API, skip user-level access checks
149+ if ( _currentContext . IdentityClientType == IdentityClientType . ServiceAccount ||
150+ _currentContext . IdentityClientType == IdentityClientType . Organization )
151+ {
152+ // Already verified Secrets Manager access and organization ownership above
153+ var serviceAccountResponses = versions . Select ( v => new SecretVersionResponseModel ( v ) ) ;
154+ return new ListResponseModel < SecretVersionResponseModel > ( serviceAccountResponses ) ;
155+ }
156+
157+ var userId = _userService . GetProperUserId ( User ) ;
158+ if ( ! userId . HasValue )
159+ {
160+ throw new NotFoundException ( ) ;
161+ }
162+
163+ var isAdmin = await _currentContext . OrganizationAdmin ( organizationId ) ;
164+ var accessClient = AccessClientHelper . ToAccessClient ( _currentContext . IdentityClientType , isAdmin ) ;
165+
166+ // Verify read access to all associated secrets
167+ var accessResults = await _secretRepository . AccessToSecretsAsync ( secretIds , userId . Value , accessClient ) ;
168+ if ( accessResults . Values . Any ( access => ! access . Read ) )
169+ {
170+ throw new NotFoundException ( ) ;
171+ }
172+
173+ var responses = versions . Select ( v => new SecretVersionResponseModel ( v ) ) ;
174+ return new ListResponseModel < SecretVersionResponseModel > ( responses ) ;
175+ }
176+
116177 [ HttpPut ( "secrets/{secretId}/versions/restore" ) ]
117178 public async Task < SecretResponseModel > RestoreVersionAsync ( [ FromRoute ] Guid secretId , [ FromBody ] RestoreSecretVersionRequestModel request )
118179 {
@@ -228,13 +289,10 @@ public async Task<IActionResult> BulkDeleteAsync([FromBody] List<Guid> ids)
228289 var accessClient = AccessClientHelper . ToAccessClient ( _currentContext . IdentityClientType , orgAdmin ) ;
229290
230291 // Verify write access to all associated secrets
231- foreach ( var secretId in secretIds )
292+ var accessResults = await _secretRepository . AccessToSecretsAsync ( secretIds , userId . Value , accessClient ) ;
293+ if ( accessResults . Values . Any ( access => ! access . Write ) )
232294 {
233- var access = await _secretRepository . AccessToSecretAsync ( secretId , userId . Value , accessClient ) ;
234- if ( ! access . Write )
235- {
236- throw new NotFoundException ( ) ;
237- }
295+ throw new NotFoundException ( ) ;
238296 }
239297
240298 await _secretVersionRepository . DeleteManyByIdAsync ( ids ) ;
0 commit comments