Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ class ContainerController {
final data = containerService.loadContainerRecord(requestId)
if( !data )
return HttpResponse.notFound()
return HttpResponse.ok(data)
return HttpResponse.ok(data.withUserIdOnly())
}

@Get('/v1alpha2/container/{requestId}/status')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class ViewController {
binding.mirror_target_image = result.targetImage
binding.mirror_platform = result.platform ?: '(all)'
binding.mirror_digest = result.digest ?: '-'
binding.mirror_user = result.userName ?: '-'
binding.mirror_user_id = result.userId != null ? result.userId.toString() : '-'
binding.put('server_url', serverUrl)
ChildRefs.populateScanBinding(binding, result.scanId, null, result.succeeded(), serverUrl)
return binding
Expand Down Expand Up @@ -242,7 +242,7 @@ class ViewController {
binding.build_failed = result.done() && !result.succeeded()
binding.build_in_progress = !result.done()
binding.build_exit_status = result.exitStatus != null ? result.exitStatus : '-'
binding.build_user = (result.userName ?: '-')
binding.build_user_id = result.userId != null ? result.userId.toString() : '-'
binding.build_time = formatTimestamp(result.startTime, result.offsetId) ?: '-'
binding.build_duration = formatDuration(result.duration) ?: '-'
binding.build_image = result.targetImage
Expand Down Expand Up @@ -299,8 +299,6 @@ class ViewController {

// user & tower data
binding.tower_user_id = data.user?.id
binding.tower_user_email = data.user?.email
binding.tower_user_name = data.user?.userName
binding.tower_workspace_id = data.workspaceId ?: '-'
binding.tower_endpoint = data.towerEndpoint

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class DescribeWaveContainerResponse {
RequestInfo() {}

RequestInfo(WaveContainerRecord data) {
this.user = data.user
// only expose the user id, stripping the user name and email
this.user = data.user ? User.ofId(data.user.id) : null
this.workspaceId = data.workspaceId
this.containerImage = data.containerImage
this.containerConfig = data.containerConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,47 @@ class WaveContainerRecord {
this.waveDigest = waveDigest
}

/**
* Copy constructor replacing the {@link User} identity, keeping all other fields unchanged.
*/
WaveContainerRecord(WaveContainerRecord that, User user) {
this.id = that.id
this.user = user
this.workspaceId = that.workspaceId
this.containerImage = that.containerImage
this.containerConfig = that.containerConfig
this.platform = that.platform
this.towerEndpoint = that.towerEndpoint
this.buildRepository = that.buildRepository
this.cacheRepository = that.cacheRepository
this.fingerprint = that.fingerprint
this.timestamp = that.timestamp
this.zoneId = that.zoneId
this.ipAddress = that.ipAddress
this.condaFile = that.condaFile
this.containerFile = that.containerFile
this.sourceImage = that.sourceImage
this.sourceDigest = that.sourceDigest
this.waveImage = that.waveImage
this.waveDigest = that.waveDigest
this.expiration = that.expiration
this.buildId = that.buildId
this.buildNew = that.buildNew
this.freeze = that.freeze
this.fusionVersion = that.fusionVersion
this.mirror = that.mirror
this.scanId = that.scanId
this.scanChildIds = that.scanChildIds
}

/**
* @return A copy of this record exposing only the user id, so that the user name
* and email are not leaked when the record is returned in an API response.
*/
WaveContainerRecord withUserIdOnly() {
return user!=null ? new WaveContainerRecord(this, User.ofId(user.id)) : this
}

/**
* Required by jackson ser/de-ser
*/
Expand Down
13 changes: 13 additions & 0 deletions src/main/groovy/io/seqera/wave/tower/User.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,17 @@ class User implements MoshiSerializable {
*/
WaveBuildNotification waveBuildNotification

/**
* Create a {@link User} instance carrying only the user id, so that the user name
* and email are not exposed when the identity is included in API responses.
*
* @param id The Tower user id, or {@code null}
* @return A {@link User} holding the given id and no other details
*/
static User ofId(Long id) {
final result = new User()
result.id = id
return result
}

}
4 changes: 2 additions & 2 deletions src/main/resources/io/seqera/wave/build-view.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
<td>{{build_id}}</td>
</tr>
<tr>
<td>User</td>
<td>{{build_user}}</td>
<td>User ID</td>
<td>{{build_user_id}}</td>
</tr>
<tr>
<td>Container image</td>
Expand Down
12 changes: 2 additions & 10 deletions src/main/resources/io/seqera/wave/container-view.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,11 @@
<h2 class="section-title">Platform &amp; User info</h2>
<table>
<tr>
<td>User Id</td>
<td>User ID</td>
<td>{{tower_user_id}}</td>
</tr>
<tr>
<td>User name</td>
<td>{{tower_user_name}}</td>
</tr>
<tr>
<td>User email</td>
<td>{{tower_user_email}}</td>
</tr>
<tr>
<td>Workspace Id</td>
<td>Workspace ID</td>
<td>{{tower_workspace_id}}</td>
</tr>
<tr>
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/io/seqera/wave/mirror-view.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
<td>{{mirror_id}}</td>
</tr>
<tr>
<td>User</td>
<td>{{mirror_user}}</td>
<td>User ID</td>
<td>{{mirror_user_id}}</td>
</tr>
<tr>
<td>Source container</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class ViewControllerTest extends Specification {
binding.build_containerfile == 'FROM foo'
binding.build_condafile == 'conda::foo'
binding.build_image == 'docker.io/some:image'
binding.build_user == 'paolo'
binding.build_user_id == '100'
binding.build_platform == 'linux/amd64'
binding.build_exit_status == 0
binding.build_platform == 'linux/amd64'
Expand All @@ -148,9 +148,9 @@ class ViewControllerTest extends Specification {
buildId: '112233_1',
dockerFile: 'FROM docker.io/test:foo',
targetImage: 'test',
userName: 'test',
userEmail: 'test',
userId: 1,
userName: 'visible-build-user',
userEmail: 'visible-build-user@example.com',
userId: 1001,
requestIp: '127.0.0.1',
startTime: Instant.now(),
duration: Duration.ofSeconds(1),
Expand All @@ -166,6 +166,10 @@ class ViewControllerTest extends Specification {
and:
response.body().contains('Container file')
response.body().contains('FROM docker.io/test:foo')
response.body().contains('User ID')
response.body().contains('1001')
!response.body().contains('visible-build-user')
!response.body().contains('visible-build-user@example.com')
and:
!response.body().contains('Conda file')
and:
Expand Down Expand Up @@ -216,7 +220,7 @@ class ViewControllerTest extends Specification {
fingerprint: 'xyz',
timestamp: Instant.now().toString() )
and:
def user = new User(id:1)
def user = new User(id:1, userName: 'container-view-user', email: 'container-view-user@example.com')
def identity = new PlatformId(user,100)
and:
def token = '12345'
Expand All @@ -237,6 +241,15 @@ class ViewControllerTest extends Specification {
response.body().contains(token)
response.body().contains(token)
and:
response.body().contains('User ID')
response.body().contains('Workspace ID')
response.body().contains('Endpoint')
response.body().contains('https://tower.nf')
!response.body().contains('container-view-user')
!response.body().contains('container-view-user@example.com')
!response.body().contains('User name')
!response.body().contains('User email')
and:
response.body().contains(serverUrl)
}

Expand Down Expand Up @@ -302,7 +315,7 @@ class ViewControllerTest extends Specification {
binding.build_containerfile == 'FROM foo'
binding.build_condafile == 'conda::foo'
binding.build_image == 'docker.io/some:image'
binding.build_user == 'paolo'
binding.build_user_id == '100'
binding.build_platform == 'linux/amd64'
binding.build_exit_status == '-'
binding.build_platform == 'linux/amd64'
Expand Down Expand Up @@ -344,7 +357,7 @@ class ViewControllerTest extends Specification {
binding.build_containerfile == 'FROM foo'
binding.build_condafile == 'conda::foo'
binding.build_image == 'docker.io/some:image'
binding.build_user == 'paolo'
binding.build_user_id == '100'
binding.build_platform == 'linux/amd64'
binding.build_exit_status == 1
binding.build_platform == 'linux/amd64'
Expand Down Expand Up @@ -446,7 +459,7 @@ class ViewControllerTest extends Specification {
"GMT",
'pditommaso',
'paolo@me.com',
1,
987654,
"sc-12456",
MirrorResult.Status.COMPLETED,
Duration.ofMinutes(1),
Expand All @@ -465,7 +478,10 @@ class ViewControllerTest extends Specification {
response.body().contains(record1.sourceImage)
response.body().contains(record1.targetImage)
response.body().contains(record1.digest)
response.body().contains(record1.userName)
response.body().contains('User ID')
response.body().contains(record1.userId.toString())
!response.body().contains(record1.userName)
!response.body().contains(record1.userEmail)
response.body().contains(serverUrl)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2026, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.exchange

import java.time.Instant

import spock.lang.Specification
import io.seqera.wave.api.SubmitContainerTokenRequest
import io.seqera.wave.core.ContainerPlatform
import io.seqera.wave.service.persistence.WaveContainerRecord
import io.seqera.wave.service.request.ContainerRequest
import io.seqera.wave.tower.PlatformId
import io.seqera.wave.tower.User
import io.seqera.wave.util.JacksonHelper

/**
* @author : jorge <jorge.aguilera@seqera.io>
*/
class DescribeWaveContainerResponseTest extends Specification {

def 'should describe user identity without name or email'() {
given:
final request = new SubmitContainerTokenRequest(
towerEndpoint: 'https://tower.nf',
towerWorkspaceId: 100,
containerImage: 'hello-world',
containerPlatform: ContainerPlatform.DEFAULT.toString(),
fingerprint: 'xyz',
timestamp: Instant.parse('2026-01-01T00:00:00Z').toString())
and:
final user = new User(id: 1, userName: 'api-user-name', email: 'api-user-name@example.com')
final data = ContainerRequest.of(
requestId: 'cr-12345',
identity: new PlatformId(user, 100),
containerImage: 'docker.io/library/hello-world:latest')
final record = new WaveContainerRecord(
request,
data,
'https://wave.io/wt/cr-12345/library/hello-world:latest',
'127.0.0.1',
Instant.parse('2026-01-02T00:00:00Z'))

when:
final response = DescribeWaveContainerResponse.create('cr-12345', record)
final json = JacksonHelper.toJson(response)

then:
response.request.user.id == 1
response.request.workspaceId == 100
response.request.towerEndpoint == 'https://tower.nf'
and:
json.contains('"user":{"id":1}')
json.contains('"workspaceId":100')
json.contains('"towerEndpoint":"https://tower.nf"')
!json.contains('api-user-name')
!json.contains('api-user-name@example.com')
!json.contains('userName')
!json.contains('email')
}
}
Loading