Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve container credentials retrieval matching container repository names #278

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
aa06fdf
added repository parser
munishchouhan Aug 8, 2023
f081df9
added repository login
munishchouhan Aug 8, 2023
138d65f
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 8, 2023
521cb46
reused some logic
munishchouhan Aug 8, 2023
5c7630c
UTs fixed
munishchouhan Aug 8, 2023
1622402
formatted
munishchouhan Aug 9, 2023
ed5dc83
formatted
munishchouhan Aug 9, 2023
fa131a0
best match algo added
munishchouhan Aug 10, 2023
2ae164f
'container-reg' check added in best match algo
munishchouhan Aug 10, 2023
c559360
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 11, 2023
50c52f1
removed repository login
munishchouhan Aug 11, 2023
5712f43
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 16, 2023
69f6925
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 21, 2023
f9072ab
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 25, 2023
99b9d2c
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 28, 2023
821005c
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Aug 31, 2023
2a1ef2a
merge master
munishchouhan Sep 8, 2023
5b2aaa9
minor change
munishchouhan Sep 8, 2023
045067b
minor change
munishchouhan Sep 8, 2023
bc84cb5
Merge remote-tracking branch 'origin/master' into 224-improve-contain…
munishchouhan Sep 15, 2023
e59fc14
master merged
munishchouhan Oct 10, 2023
b2c4e98
Merge branch 'master' into 224-improve-container-credentials-retrieva…
pditommaso Oct 29, 2023
541ff1a
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 7, 2023
92dfedc
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 13, 2023
7eba402
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 17, 2023
7c55158
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 20, 2023
23c87bf
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 21, 2023
36aac46
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 27, 2023
63ee00e
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Nov 29, 2023
6260b55
Merge branch 'master' into 224-improve-container-credentials-retrieva…
pditommaso Dec 20, 2023
1468dcc
Merge branch 'master' into 224-improve-container-credentials-retrieva…
pditommaso Dec 20, 2023
2dd789b
wip
pditommaso Dec 21, 2023
f8eed91
wip2
pditommaso Dec 21, 2023
57a1d08
wip3
pditommaso Dec 21, 2023
c59f5d8
wip4
pditommaso Dec 21, 2023
4fbf3bb
wip5
pditommaso Dec 21, 2023
bfb4ab4
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Jan 9, 2024
259657f
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Jan 11, 2024
9d78d42
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Jan 16, 2024
3d74734
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Jan 29, 2024
d5d09ad
master merged
munishchouhan Apr 1, 2024
880ed7c
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Apr 4, 2024
6757b10
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan May 30, 2024
7bb7504
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Jun 11, 2024
689a425
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Jul 9, 2024
42e7a0a
Merge branch 'master' into 224-improve-container-credentials-retrieva…
munishchouhan Aug 30, 2024
d0709aa
fixed errors
munishchouhan Aug 30, 2024
f412a8f
fixed tests
munishchouhan Aug 30, 2024
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
9 changes: 7 additions & 2 deletions src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ class RegistryAuth {
return type==Type.Bearer
}

URI getEndpoint() {
URI getEndpoint(String account=null,String repository=null) {
if( !realm )
return null
final uri = realm.toString()
if( uri?.endsWith('.amazonaws.com/') )
return new URI(uri + "v2/")
return new URI(service ? "$uri?service=${service}".toString() : uri)
def query = "service=${service}"
if( account )
query += "&account=$account"
if( repository )
query += "&scope=${URLEncoder.encode("repository:$repository:pull",'UTF-8')}"
return new URI(service ? "$uri?$query".toString() : uri)
}

static RegistryAuth parse(String auth) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ interface RegistryAuthService {
/**
* Perform a registry login
*
* @param registry The registry to login against which e.g. {@code docker.io}
* @param registry
* The registry to login against which e.g. {@code docker.io} or a container
* repository e.g. {@code docker.io/library/ubuntu}
* @param user The registry username
* @param password The registry password or PAT
* @return {@code true} if the login was successful or {@code false} otherwise
Expand All @@ -38,7 +40,9 @@ interface RegistryAuthService {
/**
* Check if the provided credentials are valid
*
* @param registry The registry to check the credentials which e.g. {@code docker.io}
* @param registry
* The registry to check the credentials which e.g. {@code docker.io} or a container
* repository e.g. {@code docker.io/library/ubuntu}
* @param user The registry username
* @param password The registry password or PAT
* @return {@code true} if the login was successful or {@code false} otherwise
Expand Down
32 changes: 27 additions & 5 deletions src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import groovy.transform.ToString
import groovy.util.logging.Slf4j
import io.seqera.wave.configuration.HttpClientConfig
import io.seqera.wave.http.HttpClientFactory
import io.seqera.wave.model.ContainerCoordinates
import io.seqera.wave.util.RegHelper
import io.seqera.wave.util.Retryable
import io.seqera.wave.util.StringUtils
Expand Down Expand Up @@ -114,7 +115,8 @@ class RegistryAuthServiceImpl implements RegistryAuthService {
/**
* Implements container registry login
*
* @param registryName The registry name e.g. docker.io or quay.io
* @param registryName
* The registry name e.g. docker.io or quay.io or a repository name
* @param username The registry username
* @param password The registry password
* @return {@code true} if the login was successful or {@code false} otherwise
Expand All @@ -125,18 +127,20 @@ class RegistryAuthServiceImpl implements RegistryAuthService {
if( !registryName )
registryName = DOCKER_IO

final target = TargetInfo.parse(registryName)

// 1. look up the registry authorisation info for the given registry name
final registry = lookupService.lookup(registryName)
log.debug "Registry '$registryName' => auth: $registry"
final registry = lookupService.lookup(target.registry)
log.debug "Registry '$target.registry' => auth: $registry"

// 2. get the registry credentials
// this is needed because some services e.g. AWS ECR requires the use of temporary tokens
final creds = credentialsFactory.create(registryName, username, password)
final creds = credentialsFactory.create(target.registry, username, password)

// 3. make a request against the authorization "realm" service using basic
// credentials to get the login token
final basic = "${creds.username}:${creds.password}".bytes.encodeBase64()
final endpoint = registry.auth.endpoint
final endpoint = registry.auth.getEndpoint()
HttpRequest request = HttpRequest.newBuilder()
.uri(endpoint)
.GET()
Expand Down Expand Up @@ -285,10 +289,28 @@ class RegistryAuthServiceImpl implements RegistryAuthService {
tokenStore.remove(getStableKey(key))
}

@Canonical
static class TargetInfo {
String registry
String repository

static TargetInfo parse(String registryOrRepository) {
assert registryOrRepository, "Missing 'registryOrRepository' argument"
if( registryOrRepository.contains('/') ) {
final coords = ContainerCoordinates.parse(registryOrRepository)
return new TargetInfo(coords.getRegistry(), coords.getImage())
}
else {
return new TargetInfo(registryOrRepository)
}
}
}

/**
* Invalidate all cached authorization tokens
*/
private static String getStableKey(CacheKey key) {
return "key-" + key.stableKey()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package io.seqera.wave.auth

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import io.micronaut.core.annotation.Nullable
import io.seqera.wave.configuration.BuildConfig
import io.seqera.wave.core.ContainerPath
Expand Down Expand Up @@ -118,14 +119,13 @@ class RegistryCredentialsProviderImpl implements RegistryCredentialsProvider {
// for a repo having the same registry host
if( container.sameRegistry(buildConfig.defaultBuildRepository) || container.sameRegistry(buildConfig.defaultCacheRepository) || container.sameRegistry(buildConfig.defaultPublicRepository) )
return getDefaultCredentials(container)

return getUserCredentials0(container.registry, identity)
return getUserCredentials0(container, identity)
}

protected RegistryCredentials getUserCredentials0(String registry, PlatformId identity) {
final keys = credentialsService.findRegistryCreds(registry, identity)
protected RegistryCredentials getUserCredentials0(ContainerPath container, PlatformId identity) {
final keys = credentialsService.findRegistryCreds(container, identity)
final result = keys
? credentialsFactory.create(registry, keys.userName, keys.password)
? credentialsFactory.create(container.registry, keys.userName, keys.password)
// create a missing credentials class with a unique key (the access token) because even when
// no credentials are provided a registry auth token token can be associated to this user
: new MissingCredentials(identity.accessToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface RegistryLookupService {
* auth endpoint
*
* @param registry
* The registry name e.g. {@code docker.io} or {@code quay.io}
* The registry name e.g. {@code docker.io} or {@code quay.io}
* @return The corresponding {@link RegistryAuth} object holding the realm URI and service info,
* or {@code null} if nothing is found
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class RegistryLookupServiceImpl implements RegistryLookupService {
* @return the corresponding registry endpoint uri
*/
protected URI registryEndpoint(String registry) {
def result = registry ?: DOCKER_IO
String result = registry ?: DOCKER_IO
if( result==DOCKER_IO )
result = DOCKER_REGISTRY_1
if( !result.startsWith('http://') && !result.startsWith('https://') )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.micronaut.http.annotation.Post
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import io.seqera.wave.auth.RegistryAuthService
import io.seqera.wave.exchange.ValidateRegistryCredsRequest
import jakarta.inject.Inject
import reactor.core.publisher.Mono

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,34 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.controller
package io.seqera.wave.exchange

import io.micronaut.core.annotation.Nullable
import javax.validation.constraints.NotBlank

import io.micronaut.core.annotation.Introspected

/**
* Request object to valide a container registry credentials

Check failure on line 26 in src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy

View workflow job for this annotation

GitHub Actions / Check for spelling errors

valide ==> valid
*/
@Introspected
class ValidateRegistryCredsRequest {

/**
* The registry user name
*/
@NotBlank
String userName

/**
* The registry password
*/
@NotBlank
String password

/**
* The registry name e.g. @{code docker.io} or a registry including the repository
* name e.g. {@code docker.io/user/repository} (without tag extension)
*/
@NotBlank
String registry
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.seqera.tower.crypto.AsymmetricCipher
import io.seqera.tower.crypto.EncryptedPacket
import io.seqera.wave.core.ContainerPath
import io.seqera.wave.service.aws.AwsEcrService
import io.seqera.wave.service.pairing.PairingService
import io.seqera.wave.tower.client.CredentialsDescription
import io.seqera.wave.tower.PlatformId
import io.seqera.wave.tower.auth.JwtAuth
import io.seqera.wave.tower.client.CredentialsDescription
import io.seqera.wave.tower.client.TowerClient
import jakarta.inject.Inject
import jakarta.inject.Singleton
import static io.seqera.wave.WaveDefault.DOCKER_IO

/**
* Define operations to access container registry credentials from Tower
*
Expand All @@ -48,7 +51,7 @@ class CredentialServiceImpl implements CredentialsService {
private PairingService keyService

@Override
ContainerRegistryKeys findRegistryCreds(String registryName, PlatformId identity) {
ContainerRegistryKeys findRegistryCreds(ContainerPath container, PlatformId identity) {
if (!identity.userId)
throw new IllegalArgumentException("Missing userId parameter")
if (!identity.accessToken)
Expand Down Expand Up @@ -77,27 +80,73 @@ class CredentialServiceImpl implements CredentialsService {
// the ones associated with docker.io.
// This cannot be implemented at the moment since, in tower, container registry
// credentials are associated to the whole registry
final matchingRegistryName = registryName ?: DOCKER_IO
def creds = all.find {
it.provider == 'container-reg' && (it.registry ?: DOCKER_IO) == matchingRegistryName
}
if (!creds && identity.workflowId && AwsEcrService.isEcrHost(registryName) ) {
final repo = container.repository ?: DOCKER_IO
def creds = findBestMatchingCreds(repo, all)
if (!creds && identity.workflowId && AwsEcrService.isEcrHost(container.registry) ) {
creds = findComputeCreds(identity)
}
if (!creds) {
log.debug "No credentials matching criteria registryName=$registryName; userId=$identity.userId; workspaceId=$identity.workspaceId; workflowId=${identity.workflowId}; endpoint=$identity.towerEndpoint"
log.debug "No credentials matching criteria registryName=$container.registry; userId=$identity.userId; workspaceId=$identity.workspaceId; workflowId=${identity.workflowId}; endpoint=$identity.towerEndpoint"
return null
}

// log for debugging purposes
log.debug "Credentials matching criteria registryName=$registryName; userId=$identity.userId; workspaceId=$identity.workspaceId; endpoint=$identity.towerEndpoint => $creds"
log.debug "Credentials matching criteria registryName=$container.registry; userId=$identity.userId; workspaceId=$identity.workspaceId; endpoint=$identity.towerEndpoint => $creds"
// now fetch the encrypted key
final encryptedCredentials = towerClient.fetchEncryptedCredentials(identity.towerEndpoint, JwtAuth.of(identity), creds.id, pairing.pairingId, identity.workspaceId).get()
final privateKey = pairing.privateKey
final credentials = decryptCredentials(privateKey, encryptedCredentials.keys)
return parsePayload(credentials)
}

protected CredentialsDescription findBestMatchingCreds(String target, List<CredentialsDescription> all) {
assert target, "Missing 'target' container repository"
// take all container registry credentials
final creds = all
.findAll(it-> it.provider=='container-reg' )

// try to find an exact match
final match = creds.find(it-> it.registry==target )
if( match )
return match

// find the longest matching repository
creds.inject((CredentialsDescription)null) { best, it-> matchingLongest(target,best,it)}
}

protected CredentialsDescription matchingLongest(String target, CredentialsDescription best, CredentialsDescription candidate) {
final a = best ? matchingScore(target, best.registry) : 0
final b = matchingScore(target, candidate.registry)
return a >= b ? best : candidate
}

/**
* Return the longest matching path length of two container repositories
*
* @param target The target repository to be authenticated
* @param authority The authority repository against which the target repository should be authenticated
* @return An integer greater or equals to zero representing the long the path in the two repositories
*/
protected int matchingScore(String target, String authority) {
if( !authority )
return 0
if( !authority.contains('/') && !authority.endsWith('/*') )
authority += '/*'
if( authority.endsWith('/*') ) {
final len = authority.length()-2
if( target.startsWith(authority.substring(0,len)) )
return len
}
else if( target==authority ) {
return target.length()
}
return 0
}

protected int repoLen(String repo) {
repo ? repo.tokenize('/').size() : 0
}

CredentialsDescription findComputeCreds(PlatformId identity) {
try {
return findComputeCreds0(identity)
Expand Down
21 changes: 18 additions & 3 deletions src/main/groovy/io/seqera/wave/service/CredentialsService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,30 @@

package io.seqera.wave.service

import io.seqera.wave.core.ContainerPath
import io.seqera.wave.tower.PlatformId

/**
* Declare operations to access container registry credentials from Tower
*
* @author Paolo Di Tommaso <[email protected]>
*/
interface CredentialsService {

ContainerRegistryKeys findRegistryCreds(String registryName, PlatformId identity)

/**
*
* @param container
* The container for which the registry credentials should be retrieved
* @param userId
* The unique ID of the Tower user
* @param workspaceId
* The unique ID of the Tower workspace
* @param towerToken
* The Tower access token
* @param towerEndpoint
* The Tower endpoint
* @return
* The container registry credentials to be used to authenticate the specified container registry or repository
* or {@code null} if no match is found
*/
ContainerRegistryKeys findRegistryCreds(ContainerPath container, PlatformId identity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,21 @@ import io.seqera.wave.WaveDefault
@ToString(includePackage = false, includeNames = true)
class CredentialsDescription {

/**
* The credential record unique ID
*/
String id

/**
* The credentials provider as defined in tower. This must be {@code container-reg} for container registry
* credentials
*/
String provider

/**
* The target container registry e.g. {@code docker.io}, {@code quay.io}. Note this can also specify a full or
* partial container repository name e.g. {@code docker.io/user/foo}
*/
String registry

@JsonProperty("keys")
Expand Down
Loading