From aa06fdfa24c188e0eefebb3b3c99840b4553e326 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Tue, 8 Aug 2023 14:08:12 +0200 Subject: [PATCH 01/18] added repository parser --- .../wave/auth/RegistryAuthServiceImpl.groovy | 23 ++++++++++++++-- .../io/seqera/wave/auth/RepositoryInfo.groovy | 7 +++++ .../seqera/wave/auth/RegistryLoginTest.groovy | 26 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/main/groovy/io/seqera/wave/auth/RepositoryInfo.groovy create mode 100644 src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index 8b233a6b4..e2bc25086 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -87,7 +87,10 @@ class RegistryAuthServiceImpl implements RegistryAuthService { // 0. default to 'docker.io' when the registry name is empty if( !registryName ) registryName = DOCKER_IO - + RepositoryInfo repositoryInfo = parseURI(registryName) + if(repositoryInfo.repository) { + registryName = repositoryInfo.registry + } // 1. look up the registry authorisation info for the given registry name final registry = lookupService.lookup(registryName) log.debug "Registry '$registryName' => auth: $registry" @@ -99,7 +102,11 @@ class RegistryAuthServiceImpl implements RegistryAuthService { // 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 + def endpoint = registry.auth.endpoint + if(repositoryInfo.repository){ + endpoint = new URI("${endpoint}&scope=repository:${repositoryInfo.repository}:pull") + } + log.info("endpoint "+endpoint.toString()) HttpRequest request = HttpRequest.newBuilder() .uri(endpoint) .GET() @@ -239,5 +246,17 @@ class RegistryAuthServiceImpl implements RegistryAuthService { cacheTokens.invalidate(key) } + protected RepositoryInfo parseURI(String endpoint){ + def pattern = /^(.*?:\/\/)?([^\/]+\/)?([^\/]+\/.*?)$/ + def matcher = (endpoint =~ pattern) + RepositoryInfo repositoryInfo = new RepositoryInfo() + if (matcher.matches()) { + repositoryInfo.protocol = matcher.group(1) ?: "https://" // Default to HTTPS + repositoryInfo.registry = matcher.group(2)?.replaceAll("/", "") ?: DOCKER_IO + repositoryInfo.repository = matcher.group(3) + } + + return repositoryInfo + } } diff --git a/src/main/groovy/io/seqera/wave/auth/RepositoryInfo.groovy b/src/main/groovy/io/seqera/wave/auth/RepositoryInfo.groovy new file mode 100644 index 000000000..eab0a6d74 --- /dev/null +++ b/src/main/groovy/io/seqera/wave/auth/RepositoryInfo.groovy @@ -0,0 +1,7 @@ +package io.seqera.wave.auth + +class RepositoryInfo { + String protocol + String registry + String repository +} diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy new file mode 100644 index 000000000..10cccec44 --- /dev/null +++ b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy @@ -0,0 +1,26 @@ +package io.seqera.wave.auth + +import spock.lang.Specification + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject + +@MicronautTest +class RegistryLoginTest extends Specification{ + @Inject + RegistryAuthServiceImpl impl + + void 'test login with registry'() { + when: + def login = impl.login("docker.io","wavetest","dckr_pat_sShAQOWshE-y3SeE8wll774CWzM") + + then: + login + } + void 'test login with repository'() { + when: + def login = impl.login("docker.io/pditommaso/wave-tests","wavetest","dckr_pat_sShAQOWshE-y3SeE8wll774CWzM") + then: + login + } +} From f081df9433ef41357d6b94542d6e4f5276bb2432 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Tue, 8 Aug 2023 21:35:01 +0200 Subject: [PATCH 02/18] added repository login --- .../wave/auth/RegistryAuthServiceImpl.groovy | 39 +++++++++++++++++-- .../seqera/wave/auth/RegistryLoginTest.groovy | 10 ++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index e2bc25086..1b7558a2c 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -87,10 +87,12 @@ class RegistryAuthServiceImpl implements RegistryAuthService { // 0. default to 'docker.io' when the registry name is empty if( !registryName ) registryName = DOCKER_IO + //check if its a repository or a registry RepositoryInfo repositoryInfo = parseURI(registryName) if(repositoryInfo.repository) { registryName = repositoryInfo.registry } + // 1. look up the registry authorisation info for the given registry name final registry = lookupService.lookup(registryName) log.debug "Registry '$registryName' => auth: $registry" @@ -103,15 +105,16 @@ class RegistryAuthServiceImpl implements RegistryAuthService { // credentials to get the login token final basic = "${creds.username}:${creds.password}".bytes.encodeBase64() def endpoint = registry.auth.endpoint - if(repositoryInfo.repository){ - endpoint = new URI("${endpoint}&scope=repository:${repositoryInfo.repository}:pull") + + if(repositoryInfo.repository) { + endpoint = new URI("${endpoint}&scope=repository:${repositoryInfo.repository}:pull") } - log.info("endpoint "+endpoint.toString()) HttpRequest request = HttpRequest.newBuilder() .uri(endpoint) .GET() .header("Authorization", "Basic $basic") .build() + log.info("request "+request.toString()) // retry strategy final retryable = Retryable .of(httpConfig) @@ -121,7 +124,14 @@ class RegistryAuthServiceImpl implements RegistryAuthService { if( response.statusCode() == 200 ) { log.debug "Container registry '$endpoint' login - response: ${StringUtils.trunc(response.body())}" - return true + + if(repositoryInfo.repository){ + final result = (Map) new JsonSlurper().parseText(response.body()) + String token = result.get('token') ?: result.get('access_token') + return loginToRepository(registry.host, repositoryInfo.repository, token) + }else{ + return true + } } else { log.warn "Container registry '$endpoint' login FAILED: ${response.statusCode()} - response: ${StringUtils.trunc(response.body())}" @@ -259,4 +269,25 @@ class RegistryAuthServiceImpl implements RegistryAuthService { return repositoryInfo } + + boolean loginToRepository(URI host, String repository, String token){ + log.info(token) + // Use the access token to access the repository + HttpRequest repositoryRequest = HttpRequest.newBuilder() + .uri(URI.create("${host}/v2/${repository}/tags/list")) + .GET() + .header("Authorization", "Bearer " + token) + .build(); + log.info(repositoryRequest.toString()) + HttpResponse repositoryResponse = httpClient.send(repositoryRequest, HttpResponse.BodyHandlers.ofString()); + + if (repositoryResponse.statusCode() == 200) { + log.info("User has access to the repository."); + log.trace("Response: " + repositoryResponse.body()); + return true + } else { + log.info("Image API request failed with status " + repositoryResponse.statusCode()); + return false + } + } } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy index 10cccec44..90467a503 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy @@ -17,10 +17,16 @@ class RegistryLoginTest extends Specification{ then: login } - void 'test login with repository'() { + void 'test valid login with repository'() { when: - def login = impl.login("docker.io/pditommaso/wave-tests","wavetest","dckr_pat_sShAQOWshE-y3SeE8wll774CWzM") + def login = impl.login("docker.io/hrma017/dev","hrma017","dckr_pat_NtfDznNlQjarjit3df4L713undw") then: login } + void 'test invalid login with repository'() { + when: + def login = impl.login("docker.io/hrma017/dev","wavetest","dckr_pat_sShAQOWshE-y3SeE8wll774CWzM") + then: + !login + } } From 521cb4626a923bcc0413ea1d8214526598e105d8 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Tue, 8 Aug 2023 21:52:54 +0200 Subject: [PATCH 03/18] reused some logic --- .../wave/auth/RegistryAuthServiceImpl.groovy | 28 +++++++++++-------- .../wave/auth/RegistryLookupService.groovy | 2 +- .../auth/RegistryLookupServiceImpl.groovy | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index 1b7558a2c..511852721 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -126,9 +126,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { log.debug "Container registry '$endpoint' login - response: ${StringUtils.trunc(response.body())}" if(repositoryInfo.repository){ - final result = (Map) new JsonSlurper().parseText(response.body()) - String token = result.get('token') ?: result.get('access_token') - return loginToRepository(registry.host, repositoryInfo.repository, token) + return loginToRepository(registry.host, repositoryInfo.repository, parseToken(response.body())) }else{ return true } @@ -211,10 +209,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { HttpResponse resp = retryable.apply(()-> httpClient.send(req, HttpResponse.BodyHandlers.ofString())) final body = resp.body() if( resp.statusCode()==200 ) { - final result = (Map) new JsonSlurper().parseText(body) - // note: azure registry returns 'access_token' - // see also specs https://docs.docker.com/registry/spec/auth/token/#requesting-a-token - final token = result.get('token') ?: result.get('access_token') + final token = parseToken(body) if( token ) { log.trace "Registry auth '$login' => token: ${StringUtils.redact(token)}" return token @@ -224,6 +219,12 @@ class RegistryAuthServiceImpl implements RegistryAuthService { throw new RegistryUnauthorizedAccessException("Unable to authorize request: $login", resp.statusCode(), body) } + String parseToken(String body){ + final result = (Map) new JsonSlurper().parseText(body) + // note: azure registry returns 'access_token' + // see also specs https://docs.docker.com/registry/spec/auth/token/#requesting-a-token + return result.get('token') ?: result.get('access_token') + } String buildLoginUrl(URI realm, String image, String service){ String result = "${realm}?scope=repository:${image}:pull" if(service) { @@ -271,14 +272,19 @@ class RegistryAuthServiceImpl implements RegistryAuthService { } boolean loginToRepository(URI host, String repository, String token){ - log.info(token) + + URI endpoint = lookupService.registryEndpoint(host.toString()) + final repositoryEndpoint = "$endpoint/${repository}/tags/list" + // Use the access token to access the repository HttpRequest repositoryRequest = HttpRequest.newBuilder() - .uri(URI.create("${host}/v2/${repository}/tags/list")) + .uri(URI.create(repositoryEndpoint)) .GET() .header("Authorization", "Bearer " + token) .build(); - log.info(repositoryRequest.toString()) + final retryable = Retryable + .of(httpConfig) + .onRetry((event) -> log.warn("Unable to connect '$repositoryEndpoint' - attempt: ${event.attemptCount}; cause: ${event.lastFailure.message}")) HttpResponse repositoryResponse = httpClient.send(repositoryRequest, HttpResponse.BodyHandlers.ofString()); if (repositoryResponse.statusCode() == 200) { @@ -286,7 +292,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { log.trace("Response: " + repositoryResponse.body()); return true } else { - log.info("Image API request failed with status " + repositoryResponse.statusCode()); + log.info("User does not have access to the repository " + repositoryResponse.statusCode()); return false } } diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy index 3a971ca0b..4af5c3f13 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy @@ -17,5 +17,5 @@ interface RegistryLookupService { * or {@code null} if nothing is found */ RegistryInfo lookup(String registry) - + URI registryEndpoint(String registry) } diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy index c7f61f8f9..3410e5c3c 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy @@ -106,7 +106,7 @@ class RegistryLookupServiceImpl implements RegistryLookupService { * @param registry The registry name e.g. quay.io. When empty defaults to 'docker.io' * @return the corresponding registry endpoint uri */ - protected URI registryEndpoint(String registry) { + URI registryEndpoint(String registry) { def result = registry ?: DOCKER_IO if( result==DOCKER_IO ) result = DOCKER_REGISTRY_1 From 5c7630cedfa4f885fe03baa430b610e051f68abc Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Tue, 8 Aug 2023 23:26:55 +0200 Subject: [PATCH 04/18] UTs fixed --- .../wave/auth/RegistryAuthServiceImpl.groovy | 20 ++++++++++++------- .../io/seqera/wave/auth/RepositoryInfo.groovy | 1 - .../seqera/wave/auth/RegistryLoginTest.groovy | 8 +++++++- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index 511852721..fd80efbb8 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -258,14 +258,20 @@ class RegistryAuthServiceImpl implements RegistryAuthService { } protected RepositoryInfo parseURI(String endpoint){ - def pattern = /^(.*?:\/\/)?([^\/]+\/)?([^\/]+\/.*?)$/ - def matcher = (endpoint =~ pattern) - RepositoryInfo repositoryInfo = new RepositoryInfo() - if (matcher.matches()) { - repositoryInfo.protocol = matcher.group(1) ?: "https://" // Default to HTTPS - repositoryInfo.registry = matcher.group(2)?.replaceAll("/", "") ?: DOCKER_IO - repositoryInfo.repository = matcher.group(3) + if(endpoint.startsWith("https://")){ + endpoint = endpoint.replace("https://","") + }else if(endpoint.startsWith("http://")){ + endpoint = endpoint.replace("http://","") + } + def parts = endpoint.split("/") + if(parts.length>1){ + repositoryInfo.registry = parts[0] + StringBuilder repo =new StringBuilder(parts[1]) + for(int i =2;i Date: Wed, 9 Aug 2023 12:28:02 +0200 Subject: [PATCH 05/18] formatted --- .../wave/auth/RegistryAuthServiceImpl.groovy | 46 +++++++++++-------- .../wave/controller/ValidateController.groovy | 1 + .../ValidateRegistryCredsRequest.groovy | 3 +- .../seqera/wave/auth/RegistryLoginTest.groovy | 7 +++ 4 files changed, 36 insertions(+), 21 deletions(-) rename src/main/groovy/io/seqera/wave/{controller => model}/ValidateRegistryCredsRequest.groovy (78%) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index fd80efbb8..5cb34cd86 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -126,6 +126,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { log.debug "Container registry '$endpoint' login - response: ${StringUtils.trunc(response.body())}" if(repositoryInfo.repository){ + // 4. make a request to repository using bearer return loginToRepository(registry.host, repositoryInfo.repository, parseToken(response.body())) }else{ return true @@ -276,30 +277,37 @@ class RegistryAuthServiceImpl implements RegistryAuthService { return repositoryInfo } - + /** + * Implements container repository login + * + * @param host The repository host e.g. https://registry-1.docker.io + * @param repository is repository name e.g. org/image + * @param token, token in the response of registry login + */ boolean loginToRepository(URI host, String repository, String token){ + URI endpoint = lookupService.registryEndpoint(host.toString()) + final repositoryEndpoint = "$endpoint/${repository}/tags/list" - URI endpoint = lookupService.registryEndpoint(host.toString()) - final repositoryEndpoint = "$endpoint/${repository}/tags/list" + // Use the access token to access the repository + HttpRequest repositoryRequest = HttpRequest.newBuilder() + .uri(URI.create(repositoryEndpoint)) + .GET() + .header("Authorization", "Bearer " + token) + .build(); - // Use the access token to access the repository - HttpRequest repositoryRequest = HttpRequest.newBuilder() - .uri(URI.create(repositoryEndpoint)) - .GET() - .header("Authorization", "Bearer " + token) - .build(); final retryable = Retryable .of(httpConfig) .onRetry((event) -> log.warn("Unable to connect '$repositoryEndpoint' - attempt: ${event.attemptCount}; cause: ${event.lastFailure.message}")) - HttpResponse repositoryResponse = httpClient.send(repositoryRequest, HttpResponse.BodyHandlers.ofString()); - - if (repositoryResponse.statusCode() == 200) { - log.info("User has access to the repository."); - log.trace("Response: " + repositoryResponse.body()); - return true - } else { - log.info("User does not have access to the repository " + repositoryResponse.statusCode()); - return false - } + //make a request + HttpResponse repositoryResponse = retryable.apply(()->httpClient.send(repositoryRequest, HttpResponse.BodyHandlers.ofString())); + + if (repositoryResponse.statusCode() == 200) { + log.info("User has access to the repository."); + log.trace("Response: " + repositoryResponse.body()); + return true + } else { + log.info("User does not have access to the repository " + repositoryResponse.statusCode()); + return false + } } } diff --git a/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy b/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy index 7ba91cff7..9d4e116a1 100644 --- a/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy +++ b/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy @@ -5,6 +5,7 @@ import javax.validation.Valid import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.seqera.wave.auth.RegistryAuthService +import io.seqera.wave.model.ValidateRegistryCredsRequest import jakarta.inject.Inject import reactor.core.publisher.Mono diff --git a/src/main/groovy/io/seqera/wave/controller/ValidateRegistryCredsRequest.groovy b/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy similarity index 78% rename from src/main/groovy/io/seqera/wave/controller/ValidateRegistryCredsRequest.groovy rename to src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy index a2a173b69..aefccc39b 100644 --- a/src/main/groovy/io/seqera/wave/controller/ValidateRegistryCredsRequest.groovy +++ b/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy @@ -1,6 +1,5 @@ -package io.seqera.wave.controller +package io.seqera.wave.model -import javax.annotation.Nullable import javax.validation.constraints.NotBlank import io.micronaut.core.annotation.Introspected diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy index 839427fd7..04f1a8719 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy @@ -35,4 +35,11 @@ class RegistryLoginTest extends Specification{ then: result.repository == null } + void 'test repository parser'(){ + when: + def result = impl.parseURI("https://docker.io/hrma017/dev") + then: + result.registry == "docker.io" + result.repository == "hrma017/dev" + } } From ed5dc83b502cf5f26366b7849a002e8882269ef2 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Wed, 9 Aug 2023 19:48:35 +0200 Subject: [PATCH 06/18] formatted --- .../wave/auth/RegistryAuthServiceImpl.groovy | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index 5cb34cd86..ed9cc262f 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -107,14 +107,15 @@ class RegistryAuthServiceImpl implements RegistryAuthService { def endpoint = registry.auth.endpoint if(repositoryInfo.repository) { - endpoint = new URI("${endpoint}&scope=repository:${repositoryInfo.repository}:pull") + endpoint = new URI("${endpoint}&scope=repository:${repositoryInfo.repository}:pull") } + HttpRequest request = HttpRequest.newBuilder() .uri(endpoint) .GET() .header("Authorization", "Basic $basic") .build() - log.info("request "+request.toString()) + // retry strategy final retryable = Retryable .of(httpConfig) @@ -220,12 +221,18 @@ class RegistryAuthServiceImpl implements RegistryAuthService { throw new RegistryUnauthorizedAccessException("Unable to authorize request: $login", resp.statusCode(), body) } + /** + * Implements a parser to extract token + * + * @param body, body of HTTPResponse + */ String parseToken(String body){ final result = (Map) new JsonSlurper().parseText(body) // note: azure registry returns 'access_token' // see also specs https://docs.docker.com/registry/spec/auth/token/#requesting-a-token return result.get('token') ?: result.get('access_token') } + String buildLoginUrl(URI realm, String image, String service){ String result = "${realm}?scope=repository:${image}:pull" if(service) { @@ -258,6 +265,11 @@ class RegistryAuthServiceImpl implements RegistryAuthService { cacheTokens.invalidate(key) } + /** + * Implements a parser to get registry and repository name from a URI + * + * @param endpoint, repository URL e.g. https://docker.io/hrma017/dev + */ protected RepositoryInfo parseURI(String endpoint){ RepositoryInfo repositoryInfo = new RepositoryInfo() if(endpoint.startsWith("https://")){ From fa131a090032236e5cb062d832d3d981a4d163ef Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 10 Aug 2023 13:04:21 +0200 Subject: [PATCH 07/18] best match algo added --- .../wave/service/CredentialServiceImpl.groovy | 32 +++++++++++++++++-- .../service/CredentialsServiceTest.groovy | 31 ++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy index 654767309..df39fa193 100644 --- a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy @@ -6,6 +6,7 @@ import groovy.util.logging.Slf4j import io.seqera.tower.crypto.AsymmetricCipher import io.seqera.tower.crypto.EncryptedPacket import io.seqera.wave.service.pairing.PairingService +import io.seqera.wave.tower.client.CredentialsDescription import io.seqera.wave.tower.client.TowerClient import jakarta.inject.Inject import jakarta.inject.Singleton @@ -57,9 +58,7 @@ class CredentialServiceImpl implements CredentialsService { // This cannot be implemented at the moment since, in tower, container registry // credentials are associated to the whole registry final matchingRegistryName = registryName ?: DOCKER_IO - final creds = all.find { - it.provider == 'container-reg' && (it.registry ?: DOCKER_IO) == matchingRegistryName - } + final creds = findBestMatchingCreds(matchingRegistryName, all) if (!creds) { log.debug "No credentials matching criteria registryName=$registryName; userId=$userId; workspaceId=$workspaceId; endpoint=$towerEndpoint" return null @@ -74,6 +73,33 @@ class CredentialServiceImpl implements CredentialsService { return parsePayload(credentials) } + //Find best match for a registry name + CredentialsDescription findBestMatchingCreds(String containerRepository, List credsList) { + int bestMatchIndex = -1 + int longestPartialMatch = 0 + + for(int i =0; i longestPartialMatch) { + longestPartialMatch = partialMatchLength + bestMatchIndex = i + } + } + } + return bestMatchIndex != -1?credsList[bestMatchIndex]:null + } + protected String decryptCredentials(byte[] encodedKey, String payload) { final packet = EncryptedPacket.decode(payload) final cipher = AsymmetricCipher.getInstance() diff --git a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy index 0efc6600f..a561f8226 100644 --- a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy @@ -173,6 +173,37 @@ class CredentialsServiceTest extends Specification { keys.password == 'you' } + def'should find the best registry match with exact match'(){ + given: + def svc = new CredentialServiceImpl() + def containerRepository = "host.com/foo/bar" + def choices = [new CredentialsDescription(registry:"host.com"), + new CredentialsDescription(registry:"host.com/foo"), + new CredentialsDescription(registry:"host.com/foo/bar"), + new CredentialsDescription(registry:"host.com/foo/bar/baz")] + + when: + def match = svc.findBestMatchingCreds(containerRepository, choices) + + then: + match.registry == "host.com/foo/bar" + } + + def'should find the best registry match with partial match'(){ + given: + def svc = new CredentialServiceImpl() + def containerRepository = "host.com/foo/bar" + def choices = [new CredentialsDescription(registry:"host.com"), + new CredentialsDescription(registry:"host.com/foo"), + new CredentialsDescription(registry:"host.com/fooo"), + new CredentialsDescription(registry:"host.com/foo/bar/baz")] + + when: + def match = svc.findBestMatchingCreds(containerRepository, choices) + + then: + match.registry == "host.com/foo" + } private static GetCredentialsKeysResponse encryptedCredentialsFromTower(PublicKey key, String credentials) { return new GetCredentialsKeysResponse(keys: TEST_CIPHER.encrypt(key,credentials.getBytes()).encode()) From 2ae164fab9537a5d6e906d9d97b8cef3c00037b6 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 10 Aug 2023 13:25:06 +0200 Subject: [PATCH 08/18] 'container-reg' check added in best match algo --- .../wave/service/CredentialServiceImpl.groovy | 24 ++++++++++--------- .../service/CredentialsServiceTest.groovy | 16 ++++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy index df39fa193..47b871454 100644 --- a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy @@ -80,20 +80,22 @@ class CredentialServiceImpl implements CredentialsService { for(int i =0; i longestPartialMatch) { - longestPartialMatch = partialMatchLength - bestMatchIndex = i + if (partialMatchLength > longestPartialMatch) { + longestPartialMatch = partialMatchLength + bestMatchIndex = i + } } } } diff --git a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy index a561f8226..4f705b3c3 100644 --- a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy @@ -177,10 +177,10 @@ class CredentialsServiceTest extends Specification { given: def svc = new CredentialServiceImpl() def containerRepository = "host.com/foo/bar" - def choices = [new CredentialsDescription(registry:"host.com"), - new CredentialsDescription(registry:"host.com/foo"), - new CredentialsDescription(registry:"host.com/foo/bar"), - new CredentialsDescription(registry:"host.com/foo/bar/baz")] + def choices = [new CredentialsDescription(registry:"host.com",provider: 'container-reg'), + new CredentialsDescription(registry:"host.com/foo",provider: 'container-reg'), + new CredentialsDescription(registry:"host.com/foo/bar", provider:'container-reg'), + new CredentialsDescription(registry:"host.com/foo/bar/baz",provider: 'container-reg')] when: def match = svc.findBestMatchingCreds(containerRepository, choices) @@ -193,10 +193,10 @@ class CredentialsServiceTest extends Specification { given: def svc = new CredentialServiceImpl() def containerRepository = "host.com/foo/bar" - def choices = [new CredentialsDescription(registry:"host.com"), - new CredentialsDescription(registry:"host.com/foo"), - new CredentialsDescription(registry:"host.com/fooo"), - new CredentialsDescription(registry:"host.com/foo/bar/baz")] + def choices = [new CredentialsDescription(registry:"host.com",provider: 'container-reg'), + new CredentialsDescription(registry:"host.com/foo",provider: 'container-reg'), + new CredentialsDescription(registry:"host.com/fooo", provider:'container-reg'), + new CredentialsDescription(registry:"host.com/foo/bar/baz",provider: 'container-reg')] when: def match = svc.findBestMatchingCreds(containerRepository, choices) From 50c52f12cbfe30928d9f84075bad657a39a5e2a7 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Fri, 11 Aug 2023 16:19:59 +0200 Subject: [PATCH 09/18] removed repository login --- .../wave/auth/RegistryAuthServiceImpl.groovy | 41 +---------------- .../wave/auth/RegistryAuthServiceTest.groovy | 30 ++++++++++--- .../seqera/wave/auth/RegistryLoginTest.groovy | 45 ------------------- 3 files changed, 24 insertions(+), 92 deletions(-) delete mode 100644 src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index ed9cc262f..539f7e77e 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -125,13 +125,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { if( response.statusCode() == 200 ) { log.debug "Container registry '$endpoint' login - response: ${StringUtils.trunc(response.body())}" - - if(repositoryInfo.repository){ - // 4. make a request to repository using bearer - return loginToRepository(registry.host, repositoryInfo.repository, parseToken(response.body())) - }else{ - return true - } + return true } else { log.warn "Container registry '$endpoint' login FAILED: ${response.statusCode()} - response: ${StringUtils.trunc(response.body())}" @@ -289,37 +283,4 @@ class RegistryAuthServiceImpl implements RegistryAuthService { return repositoryInfo } - /** - * Implements container repository login - * - * @param host The repository host e.g. https://registry-1.docker.io - * @param repository is repository name e.g. org/image - * @param token, token in the response of registry login - */ - boolean loginToRepository(URI host, String repository, String token){ - URI endpoint = lookupService.registryEndpoint(host.toString()) - final repositoryEndpoint = "$endpoint/${repository}/tags/list" - - // Use the access token to access the repository - HttpRequest repositoryRequest = HttpRequest.newBuilder() - .uri(URI.create(repositoryEndpoint)) - .GET() - .header("Authorization", "Bearer " + token) - .build(); - - final retryable = Retryable - .of(httpConfig) - .onRetry((event) -> log.warn("Unable to connect '$repositoryEndpoint' - attempt: ${event.attemptCount}; cause: ${event.lastFailure.message}")) - //make a request - HttpResponse repositoryResponse = retryable.apply(()->httpClient.send(repositoryRequest, HttpResponse.BodyHandlers.ofString())); - - if (repositoryResponse.statusCode() == 200) { - log.info("User has access to the repository."); - log.trace("Response: " + repositoryResponse.body()); - return true - } else { - log.info("User does not have access to the repository " + repositoryResponse.statusCode()); - return false - } - } } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy index 1cd4403af..65fa0f09e 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy @@ -110,13 +110,15 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis logged == VALID where: - USER | PWD | REGISTRY_URL | VALID - 'test' | 'test' | 'localhost' | true - 'nope' | 'yepes' | 'localhost' | false - dockerUsername | dockerPassword | "https://registry-1.docker.io" | true - 'nope' | 'yepes' | "https://registry-1.docker.io" | false - quayUsername | quayPassword | "https://quay.io" | true - 'nope' | 'yepes' | "https://quay.io" | false + USER | PWD | REGISTRY_URL | VALID + 'test' | 'test' | 'localhost' | true + 'nope' | 'yepes' | 'localhost' | false + dockerUsername | dockerPassword | "https://registry-1.docker.io" | true + 'nope' | 'yepes' | "https://registry-1.docker.io" | false + quayUsername | quayPassword | "https://quay.io" | true + 'nope' | 'yepes' | "https://quay.io" | false + dockerUsername | dockerPassword | "https://registry-1.docker.io/pditommaso/wave-tests" | true + dockerUsername | dockerPassword | "https://registry-1.docker.io/pditommaso" | true } @Ignore @@ -146,4 +148,18 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis 'localhost' | 'test' | null | "localhost?scope=repository:test:pull" } + void 'test repository parser'(){ + when: + def result = loginService.parseURI("localhost") + then: + result.repository == null + } + + void 'test repository parser'(){ + when: + def result = loginService.parseURI("https://docker.io/hrma017/dev") + then: + result.registry == "docker.io" + result.repository == "hrma017/dev" + } } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy deleted file mode 100644 index 04f1a8719..000000000 --- a/src/test/groovy/io/seqera/wave/auth/RegistryLoginTest.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package io.seqera.wave.auth - -import spock.lang.Specification - -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import jakarta.inject.Inject - -@MicronautTest -class RegistryLoginTest extends Specification{ - @Inject - RegistryAuthServiceImpl impl - - void 'test login with registry'() { - when: - def login = impl.login("docker.io","wavetest","dckr_pat_sShAQOWshE-y3SeE8wll774CWzM") - - then: - login - } - void 'test valid login with repository'() { - when: - def login = impl.login("https://docker.io/hrma017/dev","hrma017","dckr_pat_NtfDznNlQjarjit3df4L713undw") - then: - login - } - void 'test invalid login with repository'() { - when: - def login = impl.login("docker.io/hrma017/dev","wavetest","dckr_pat_sShAQOWshE-y3SeE8wll774CWzM") - then: - !login - } - void 'test repository parser'(){ - when: - def result = impl.parseURI("localhost") - then: - result.repository == null - } - void 'test repository parser'(){ - when: - def result = impl.parseURI("https://docker.io/hrma017/dev") - then: - result.registry == "docker.io" - result.repository == "hrma017/dev" - } -} From 5b2aaa9bab4350ce0f9bc76ba92525c425d773e2 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Fri, 8 Sep 2023 07:19:04 +0200 Subject: [PATCH 10/18] minor change --- .../io/seqera/wave/model/ValidateRegistryCredsRequest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy b/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy index 22d62251a..884338a1e 100644 --- a/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy +++ b/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy @@ -9,7 +9,7 @@ * defined by the Mozilla Public License, v. 2.0. */ -package io.seqera.wave.controllersssss +package io.seqera.wave.controller import javax.validation.constraints.NotBlank From 045067bf6f93514d0257a35e71ae2c210a359c3f Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Fri, 8 Sep 2023 07:26:20 +0200 Subject: [PATCH 11/18] minor change --- .../io/seqera/wave/model/ValidateRegistryCredsRequest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy b/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy index 884338a1e..5dde6d20e 100644 --- a/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy +++ b/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy @@ -9,7 +9,7 @@ * defined by the Mozilla Public License, v. 2.0. */ -package io.seqera.wave.controller +package io.seqera.wave.model import javax.validation.constraints.NotBlank From 2dd789bc0d39b6f1593e04060219ad6b4fe2706c Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 21 Dec 2023 10:10:21 +0100 Subject: [PATCH 12/18] wip Signed-off-by: Paolo Di Tommaso --- .../io/seqera/wave/auth/RegistryAuth.groovy | 9 +- .../wave/auth/RegistryAuthService.groovy | 8 +- .../wave/auth/RegistryAuthServiceImpl.groovy | 73 +++++--------- .../wave/auth/RegistryLookupService.groovy | 4 +- .../auth/RegistryLookupServiceImpl.groovy | 10 +- .../io/seqera/wave/auth/RepositoryInfo.groovy | 6 -- .../wave/service/CredentialServiceImpl.groovy | 78 ++++++++------- .../wave/service/CredentialsService.groovy | 14 +++ .../client/CredentialsDescription.groovy | 13 +++ .../wave/auth/RegistryAuthServiceTest.groovy | 65 ++++++------- .../auth/RegistryLookupServiceTest.groovy | 2 + .../service/CredentialsServiceTest.groovy | 95 ++++++++++++++++--- .../test/BaseTestContainerRegistry.groovy | 2 +- 13 files changed, 227 insertions(+), 152 deletions(-) delete mode 100644 src/main/groovy/io/seqera/wave/auth/RepositoryInfo.groovy diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy index 03d6a3133..ed3a818d7 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy @@ -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) { diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy index b57ccd705..883b405b1 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy @@ -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 @@ -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 diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index da3634964..f7410eec4 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -34,6 +34,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.Retryable import io.seqera.wave.util.StringUtils import jakarta.inject.Inject @@ -85,7 +86,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 @@ -95,35 +97,26 @@ class RegistryAuthServiceImpl implements RegistryAuthService { // 0. default to 'docker.io' when the registry name is empty if( !registryName ) registryName = DOCKER_IO - //check if its a repository or a registry - RepositoryInfo repositoryInfo = parseURI(registryName) - if(repositoryInfo.repository) { - registryName = repositoryInfo.registry - } + + final target = RegistryMeta.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() - def endpoint = registry.auth.endpoint - - if(repositoryInfo.repository) { - endpoint = new URI("${endpoint}&scope=repository:${repositoryInfo.repository}:pull") - } - + final endpoint = registry.auth.getEndpoint(username, target.repository) HttpRequest request = HttpRequest.newBuilder() .uri(endpoint) .GET() .header("Authorization", "Basic $basic") .build() - // retry strategy final retryable = Retryable .>of(httpConfig) @@ -234,18 +227,6 @@ class RegistryAuthServiceImpl implements RegistryAuthService { throw new RegistryUnauthorizedAccessException("Unable to authorize request: $login", response.statusCode(), body) } - /** - * Implements a parser to extract token - * - * @param body, body of HTTPResponse - */ - String parseToken(String body){ - final result = (Map) new JsonSlurper().parseText(body) - // note: azure registry returns 'access_token' - // see also specs https://docs.docker.com/registry/spec/auth/token/#requesting-a-token - return result.get('token') ?: result.get('access_token') - } - String buildLoginUrl(URI realm, String image, String service){ String result = "${realm}?scope=repository:${image}:pull" if(service) { @@ -278,28 +259,22 @@ class RegistryAuthServiceImpl implements RegistryAuthService { cacheTokens.invalidate(key) } - /** - * Implements a parser to get registry and repository name from a URI - * - * @param endpoint, repository URL e.g. https://docker.io/hrma017/dev - */ - protected RepositoryInfo parseURI(String endpoint){ - RepositoryInfo repositoryInfo = new RepositoryInfo() - if(endpoint.startsWith("https://")){ - endpoint = endpoint.replace("https://","") - }else if(endpoint.startsWith("http://")){ - endpoint = endpoint.replace("http://","") - } - def parts = endpoint.split("/") - if(parts.length>1){ - repositoryInfo.registry = parts[0] - StringBuilder repo =new StringBuilder(parts[1]) - for(int i =2;i credsList) { - int bestMatchIndex = -1 - int longestPartialMatch = 0 - - for(int i =0; i longestPartialMatch) { - longestPartialMatch = partialMatchLength - bestMatchIndex = i - } - } - } - } - return bestMatchIndex != -1?credsList[bestMatchIndex]:null + protected CredentialsDescription findBestMatchingCreds(String target, List all) { + // 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) { + final t = target ? target.tokenize('/') : List.of() + final r = authority ? authority.tokenize('/') : List.of() + // the authority repo length cannot be longer of the target repository + if( r.size()>t.size() ) + return 0 + // look for the longest matching path + int i=0 + while( i Date: Thu, 21 Dec 2023 10:34:57 +0100 Subject: [PATCH 13/18] wip2 Signed-off-by: Paolo Di Tommaso --- .../auth/RegistryCredentialsProviderImpl.groovy | 10 ++++------ .../seqera/wave/auth/RegistryLookupService.groovy | 2 +- .../wave/service/CredentialServiceImpl.groovy | 14 +++++++------- .../seqera/wave/service/CredentialsService.groovy | 11 ++++++++--- .../auth/RegistryCredentialsProviderTest.groovy | 7 ++++--- .../wave/service/CredentialsServiceTest.groovy | 15 +++++++++++---- 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy index e3f11b09c..7587f5343 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy @@ -18,11 +18,9 @@ package io.seqera.wave.auth -import javax.annotation.Nullable import groovy.transform.CompileStatic import groovy.util.logging.Slf4j -import io.micronaut.context.annotation.Value import io.seqera.wave.configuration.BuildConfig import io.seqera.wave.core.ContainerPath import io.seqera.wave.service.CredentialsService @@ -118,13 +116,13 @@ class RegistryCredentialsProviderImpl implements RegistryCredentialsProvider { if( repo==buildConfig.defaultBuildRepository || repo==buildConfig.defaultCacheRepository || repo==buildConfig.defaultPublicRepository) return getDefaultCredentials(container) - return getUserCredentials0(container.registry, userId, workspaceId, towerToken, towerEndpoint) + return getUserCredentials0(container, userId, workspaceId, towerToken, towerEndpoint) } - protected RegistryCredentials getUserCredentials0(String registry, Long userId, Long workspaceId, String towerToken, String towerEndpoint) { - final keys = credentialsService.findRegistryCreds(registry, userId, workspaceId, towerToken, towerEndpoint) + protected RegistryCredentials getUserCredentials0(ContainerPath container, Long userId, Long workspaceId, String towerToken, String towerEndpoint) { + final keys = credentialsService.findRegistryCreds(container, userId, workspaceId, towerToken, towerEndpoint) 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(towerToken) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy index b06385c8e..2e39a36ac 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy @@ -35,5 +35,5 @@ interface RegistryLookupService { * or {@code null} if nothing is found */ RegistryInfo lookup(String registry) - + } diff --git a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy index 6912d8034..036df3b59 100644 --- a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy @@ -18,17 +18,16 @@ package io.seqera.wave.service - 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.pairing.PairingService 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 * @@ -46,7 +45,7 @@ class CredentialServiceImpl implements CredentialsService { private PairingService keyService @Override - ContainerRegistryKeys findRegistryCreds(String registryName, Long userId, Long workspaceId, String towerToken, String towerEndpoint) { + ContainerRegistryKeys findRegistryCreds(ContainerPath container, Long userId, Long workspaceId, String towerToken, String towerEndpoint) { if (!userId) throw new IllegalArgumentException("Missing userId parameter") if (!towerToken) @@ -66,15 +65,15 @@ class CredentialServiceImpl implements CredentialsService { } // find credentials with a matching registry - final matchingRegistryName = registryName ?: DOCKER_IO - final creds = findBestMatchingCreds(matchingRegistryName, all) + final repo = container.repository + final creds = findBestMatchingCreds(repo, all) if (!creds) { - log.debug "No credentials matching criteria registryName=$registryName; userId=$userId; workspaceId=$workspaceId; endpoint=$towerEndpoint" + log.debug "No credentials matching criteria for repository=$repo; userId=$userId; workspaceId=$workspaceId; endpoint=$towerEndpoint" return null } // log for debugging purposes - log.debug "Credentials matching criteria registryName=$registryName; userId=$userId; workspaceId=$workspaceId; endpoint=$towerEndpoint => $creds" + log.debug "Credentials matching criteria for repository=$repo; userId=$userId; workspaceId=$workspaceId; endpoint=$towerEndpoint => $creds" // now fetch the encrypted key final encryptedCredentials = towerClient.fetchEncryptedCredentials(towerEndpoint, towerToken, creds.id, pairing.pairingId, workspaceId).get() final privateKey = pairing.privateKey @@ -83,6 +82,7 @@ class CredentialServiceImpl implements CredentialsService { } protected CredentialsDescription findBestMatchingCreds(String target, List all) { + assert target, "Missing 'target' container repository" // take all container registry credentials final creds = all .findAll(it-> it.provider=='container-reg' ) diff --git a/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy b/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy index 7db3b2998..c19b6b386 100644 --- a/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy +++ b/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy @@ -17,6 +17,9 @@ */ package io.seqera.wave.service + +import io.seqera.wave.core.ContainerPath + /** * Declare operations to access container registry credentials from Tower * @@ -26,8 +29,8 @@ interface CredentialsService { /** * - * @param registryName - * The registry or repository name for which credentials a required + * @param container + * The container for which the registry credentials should be retrieved * @param userId * The unique ID of the Tower user * @param workspaceId @@ -37,7 +40,9 @@ interface CredentialsService { * @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(String registryName, Long userId, Long workspaceId, String towerToken, String towerEndpoint) + ContainerRegistryKeys findRegistryCreds(ContainerPath container, Long userId, Long workspaceId, String towerToken, String towerEndpoint) } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy index 685edee41..104204d15 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy @@ -25,6 +25,7 @@ import spock.lang.Specification import io.micronaut.context.annotation.Value import io.micronaut.core.annotation.Nullable import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.seqera.wave.model.ContainerCoordinates import io.seqera.wave.service.ContainerRegistryKeys import io.seqera.wave.service.CredentialsService import io.seqera.wave.service.aws.AwsEcrService @@ -99,7 +100,7 @@ class RegistryCredentialsProviderTest extends Specification { def 'should get credentials from user' () { given: - def REGISTRY = 'foo' + def CONTAINER = ContainerCoordinates.parse('docker.io/foo') def USER_ID = 100 def WORKSPACE_ID = 200 def TOWER_TOKEN = "token" @@ -110,9 +111,9 @@ class RegistryCredentialsProviderTest extends Specification { def provider = Spy(new RegistryCredentialsProviderImpl(credentialsFactory: credentialsFactory, credentialsService: credentialService)) when: - def result = provider.getUserCredentials0(REGISTRY, USER_ID, WORKSPACE_ID, TOWER_TOKEN, TOWER_ENDPOINT) + def result = provider.getUserCredentials0(CONTAINER, USER_ID, WORKSPACE_ID, TOWER_TOKEN, TOWER_ENDPOINT) then: - 1 * credentialService.findRegistryCreds(REGISTRY, USER_ID, WORKSPACE_ID, TOWER_TOKEN, TOWER_ENDPOINT) >> new ContainerRegistryKeys(userName:'usr1',password:'pwd2',registry:REGISTRY) + 1 * credentialService.findRegistryCreds(CONTAINER, USER_ID, WORKSPACE_ID, TOWER_TOKEN, TOWER_ENDPOINT) >> new ContainerRegistryKeys(userName:'usr1',password:'pwd2',registry:CONTAINER) and: result.getUsername() == 'usr1' result.getPassword() == 'pwd2' diff --git a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy index f0c64d3e1..e046e54b6 100644 --- a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy @@ -30,6 +30,7 @@ import java.util.concurrent.CompletableFuture import io.micronaut.test.annotation.MockBean import io.micronaut.test.extensions.spock.annotation.MicronautTest import io.seqera.tower.crypto.AsymmetricCipher +import io.seqera.wave.model.ContainerCoordinates import io.seqera.wave.service.pairing.PairingRecord import io.seqera.wave.service.pairing.PairingService import io.seqera.wave.tower.client.CredentialsDescription @@ -95,7 +96,8 @@ class CredentialsServiceTest extends Specification { when: 'look those registry credentials from tower' - def credentials = credentialsService.findRegistryCreds("quay.io",userId, workspaceId,token,towerEndpoint) + def container = ContainerCoordinates.parse("quay.io/foo") + def credentials = credentialsService.findRegistryCreds(container,userId, workspaceId,token,towerEndpoint) then: 'the registered key is fetched correctly from the security service' 1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, towerEndpoint) >> keyRecord @@ -117,8 +119,10 @@ class CredentialsServiceTest extends Specification { def 'should fail if keys where not registered for the tower endpoint'() { + given: + def container = ContainerCoordinates.parse('quay.io/foo') when: - credentialsService.findRegistryCreds('quay.io',10,10,"token",'endpoint') + credentialsService.findRegistryCreds(container,10,10,"token",'endpoint') then: 'the security service does not have the key for the hostname' 1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE,'endpoint') >> null @@ -128,8 +132,10 @@ class CredentialsServiceTest extends Specification { } def 'should return no registry credentials if the user has no credentials in tower' () { + given: + def container = ContainerCoordinates.parse('quay.io/foo') when: - def credentials = credentialsService.findRegistryCreds('quay.io', 10, 10, "token",'tower.io') + def credentials = credentialsService.findRegistryCreds(container, 10, 10, "token",'tower.io') then: 'a key is found' 1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, 'tower.io') >> new PairingRecord( pairingId: 'a-key-id', @@ -159,7 +165,8 @@ class CredentialsServiceTest extends Specification { ) when: - def credentials = credentialsService.findRegistryCreds('quay.io', 10, 10, "token",'tower.io') + def container = ContainerCoordinates.parse('quay.io/foo') + def credentials = credentialsService.findRegistryCreds(container, 10, 10, "token",'tower.io') then: 'a key is found' 1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, 'tower.io') >> new PairingRecord( From 57a1d083bb53336b72323943da4092aa7a279b97 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 21 Dec 2023 15:24:49 +0100 Subject: [PATCH 14/18] wip3 Signed-off-by: Paolo Di Tommaso --- .../wave/service/CredentialServiceImpl.groovy | 21 +++--- .../service/CredentialsServiceTest.groovy | 64 ++++++++----------- 2 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy index 036df3b59..1d732d9e8 100644 --- a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy @@ -110,16 +110,19 @@ class CredentialServiceImpl implements CredentialsService { * @return An integer greater or equals to zero representing the long the path in the two repositories */ protected int matchingScore(String target, String authority) { - final t = target ? target.tokenize('/') : List.of() - final r = authority ? authority.tokenize('/') : List.of() - // the authority repo length cannot be longer of the target repository - if( r.size()>t.size() ) + if( !authority ) return 0 - // look for the longest matching path - int i=0 - while( i new CredentialsDescription(registry: it, provider: 'container-reg') ) when: def match = svc.findBestMatchingCreds(target, choices) then: - match.registry == "host.com/foo" - match.provider == "container-reg" - } - - def'should find the best registry match with invalid partial match'(){ - given: - def svc = new CredentialServiceImpl() - def target = "host.com/foo/bar" - def choices = [ - new CredentialsDescription(registry:"host.com",provider: 'container-reg'), - new CredentialsDescription(registry:"host.com/fo",provider: 'container-reg'), - new CredentialsDescription(registry:"host.com/fooo", provider:'container-reg'), - new CredentialsDescription(registry:"host.com/foo/bar/baz",provider: 'container-reg')] - - when: - def match = svc.findBestMatchingCreds(target, choices) + match.registry == EXPECTED - then: - match.registry == "host.com" - match.provider == "container-reg" + where: + TARGET | EXPECTED | CHOICES + "host.com" | 'host.com' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*' + "host.com/fo" | 'host.com' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*' + "host.com/bar" | 'host.com' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*' + "host.com/foo" | 'host.com/foo' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*' + "host.com/foo/bar" | 'host.com/foo/*' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*' + "host.com/foo/bar/baz" | 'host.com/foo/bar/baz/*' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*' } + private static GetCredentialsKeysResponse encryptedCredentialsFromTower(PublicKey key, String credentials) { return new GetCredentialsKeysResponse(keys: TEST_CIPHER.encrypt(key,credentials.getBytes()).encode()) } @@ -267,29 +253,30 @@ class CredentialsServiceTest extends Specification { def svc = new CredentialServiceImpl() expect: - svc.matchingScore(TARGET, REPO) == EXPECTED + svc.matchingScore(TARGET, PATTERN) == EXPECTED where: - TARGET | REPO | EXPECTED + TARGET | PATTERN | EXPECTED null | null | 0 'quay.io' | null | 0 'quay.io' | 'docker.io' | 0 and: - 'quay.io' | 'quay.io' | 1 - 'quay.io/foo' | 'quay.io' | 1 - 'quay.io/foo/bar' | 'quay.io' | 1 - 'quay.io/foo/bar' | 'quay.io/fo' | 1 - 'quay.io/foo/bar' | 'quay.io/fooo' | 1 + 'quay.io' | 'quay.io' | 'quay.io'.length() + 'quay.io/foo' | 'quay.io' | 'quay.io'.length() + 'quay.io/foo/bar' | 'quay.io' | 'quay.io'.length() and: - 'quay.io/foo/bar' | 'quay.io/foo' | 2 + 'quay.io/foo/bar' | 'quay.io/fo' | 0 + 'quay.io/foo/bar' | 'quay.io/fooo' | 0 + 'quay.io/foo/bar' | 'quay.io/*' | 'quay.io'.length() and: - 'quay.io/foo/bar' | 'quay.io/foo/bar' | 3 + 'quay.io/foo' | 'quay.io/foo/*' | 'quay.io/foo'.length() + 'quay.io/foo/bar' | 'quay.io/foo/*' | 'quay.io/foo'.length() and: // should should return 0 because the "authority" repository has // a longer name of the target one. Therefore it cannot be used // to authenticate the target - 'quay.io' | 'quay.io/foo/bar' | 0 - 'quay.io/foo' | 'quay.io/foo/bar' | 0 + 'quay.io' | 'quay.io/foo/*' | 0 + 'quay.io/fo/bar' | 'quay.io/foo/*' | 0 } @@ -303,6 +290,7 @@ class CredentialsServiceTest extends Specification { TARGET | R1 | R2 | EXPECTED 'docker.io' | 'docker.io' | 'quay.io' | 'docker.io' 'docker.io/foo' | 'docker.io/foo' | 'docker.io' | 'docker.io/foo' - 'docker.io/foo/bar' | 'docker.io/foo' | 'docker.io' | 'docker.io/foo' + 'docker.io/foo/bar' | 'docker.io/foo/*' | 'docker.io' | 'docker.io/foo/*' + 'docker.io/foo/bar' | 'docker.io/foo' | 'docker.io' | 'docker.io' } } From c59f5d8e1d82173d22136757ee729d7bea970b34 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 21 Dec 2023 15:54:25 +0100 Subject: [PATCH 15/18] wip4 Signed-off-by: Paolo Di Tommaso --- .../wave/auth/RegistryAuthServiceTest.groovy | 6 +++--- .../ValidateCredsControllerTest.groovy | 20 +++++++++---------- .../test/BaseTestContainerRegistry.groovy | 6 +++++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy index c2e12201b..cb2af51be 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy @@ -74,7 +74,7 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis void 'test valid login'() { given: - String uri = getTestRegistryUrl(REGISTRY) + String uri = getTestRegistryName(REGISTRY) when: boolean logged = loginService.login(uri, USER, PWD) @@ -119,10 +119,10 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis void 'test containerService valid login'() { given: - String uri = getTestRegistryUrl(REGISTRY) + String registry = getTestRegistryName(REGISTRY) when: - boolean logged = loginService.validateUser(uri, USER, PWD) + boolean logged = loginService.validateUser(registry, USER, PWD) then: logged == VALID diff --git a/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy b/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy index 02d64350d..3fae48f74 100644 --- a/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy +++ b/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy @@ -90,7 +90,7 @@ class ValidateCredsControllerTest extends Specification implements SecureDockerR def req = [ userName:'test', password:'test', - registry: getTestRegistryUrl('test') ] + registry: getTestRegistryName('test') ] and: HttpRequest request = HttpRequest.POST("/validate-creds", req) when: @@ -106,7 +106,7 @@ class ValidateCredsControllerTest extends Specification implements SecureDockerR def req = [ userName: USER, password: PWD, - registry: getTestRegistryUrl(REGISTRY_URL) + registry: getTestRegistryName(REGISTRY) ] HttpRequest request = HttpRequest.POST("/validate-creds", req) when: @@ -118,13 +118,13 @@ class ValidateCredsControllerTest extends Specification implements SecureDockerR response.body() == VALID where: - USER | PWD | REGISTRY_URL | VALID - 'test' | 'test' | 'test' | true - 'nope' | 'yepes' | 'test' | false - dockerUsername | dockerPassword | "https://registry-1.docker.io" | true - 'nope' | 'yepes' | "https://registry-1.docker.io" | false - quayUsername | quayPassword | "https://quay.io" | true - 'nope' | 'yepes' | "https://quay.io" | false - 'test' | 'test' | 'test' | true + USER | PWD | REGISTRY | VALID + 'test' | 'test' | 'test' | true + 'nope' | 'yepes' | 'test' | false + dockerUsername | dockerPassword | "registry-1.docker.io" | true + 'nope' | 'yepes' | "registry-1.docker.io" | false + quayUsername | quayPassword | "quay.io" | true + 'nope' | 'yepes' | "quay.io" | false + 'test' | 'test' | 'test' | true } } diff --git a/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy b/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy index 6a776eeaa..e0329384a 100644 --- a/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy +++ b/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy @@ -31,7 +31,7 @@ trait BaseTestContainerRegistry { abstract GenericContainer getTestcontainers() - String getTestRegistryUrl(String registry=null) { + String getTestRegistryName(String registry=null) { if( !registry || registry=='test' || registry=='localhost' ) { int port = testcontainers.firstMappedPort return "${testcontainers.getHost()}:$port" @@ -40,6 +40,10 @@ trait BaseTestContainerRegistry { return registry } + String getTestRegistryUrl(String registry=null) { + return "http://" + getTestRegistryName(registry) + } + RegistryInfo getLocalTestRegistryInfo() { final uri = new URI(getTestRegistryUrl()) new RegistryInfo('test', uri, new RegistryAuth(uri, null, RegistryAuth.Type.Basic)) From 4fbf3bb25d6b725780ed2254a893d8c7fc17adfe Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 21 Dec 2023 17:03:46 +0100 Subject: [PATCH 16/18] wip5 Signed-off-by: Paolo Di Tommaso --- .../wave/auth/RegistryAuthServiceImpl.groovy | 12 ++++++------ .../wave/controller/ValidateController.groovy | 2 +- .../ValidateRegistryCredsRequest.groovy | 18 +++++++++++++++++- .../wave/auth/RegistryAuthServiceTest.groovy | 16 ++++++++-------- 4 files changed, 32 insertions(+), 16 deletions(-) rename src/main/groovy/io/seqera/wave/{model => exchange}/ValidateRegistryCredsRequest.groovy (73%) diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy index f7410eec4..c5eee2bcd 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy @@ -98,7 +98,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { if( !registryName ) registryName = DOCKER_IO - final target = RegistryMeta.parse(registryName) + final target = TargetInfo.parse(registryName) // 1. look up the registry authorisation info for the given registry name final registry = lookupService.lookup(target.registry) @@ -111,7 +111,7 @@ class RegistryAuthServiceImpl implements RegistryAuthService { // 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.getEndpoint(username, target.repository) + final endpoint = registry.auth.getEndpoint() HttpRequest request = HttpRequest.newBuilder() .uri(endpoint) .GET() @@ -261,18 +261,18 @@ class RegistryAuthServiceImpl implements RegistryAuthService { @Canonical - static class RegistryMeta { + static class TargetInfo { String registry String repository - static RegistryMeta parse(String registryOrRepository) { + static TargetInfo parse(String registryOrRepository) { assert registryOrRepository, "Missing 'registryOrRepository' argument" if( registryOrRepository.contains('/') ) { final coords = ContainerCoordinates.parse(registryOrRepository) - return new RegistryMeta(coords.getRegistry(), coords.getImage()) + return new TargetInfo(coords.getRegistry(), coords.getImage()) } else { - return new RegistryMeta(registryOrRepository) + return new TargetInfo(registryOrRepository) } } } diff --git a/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy b/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy index 00dcb0b83..8829432eb 100644 --- a/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy +++ b/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy @@ -23,7 +23,7 @@ import javax.validation.Valid import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post import io.seqera.wave.auth.RegistryAuthService -import io.seqera.wave.model.ValidateRegistryCredsRequest +import io.seqera.wave.exchange.ValidateRegistryCredsRequest import jakarta.inject.Inject import reactor.core.publisher.Mono diff --git a/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy b/src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy similarity index 73% rename from src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy rename to src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy index e1d53b338..e62cd1fbc 100644 --- a/src/main/groovy/io/seqera/wave/model/ValidateRegistryCredsRequest.groovy +++ b/src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy @@ -16,18 +16,34 @@ * along with this program. If not, see . */ -package io.seqera.wave.model +package io.seqera.wave.exchange import javax.validation.constraints.NotBlank import io.micronaut.core.annotation.Introspected +/** + * Request object to valide a container registry credentials + */ @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 } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy index cb2af51be..3fc762bf4 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy @@ -74,10 +74,10 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis void 'test valid login'() { given: - String uri = getTestRegistryName(REGISTRY) + String registry = getTestRegistryName(REGISTRY) when: - boolean logged = loginService.login(uri, USER, PWD) + boolean logged = loginService.login(registry, USER, PWD) then: logged == VALID @@ -95,26 +95,26 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis @IgnoreIf({!System.getenv('AZURECR_USER')}) void 'test valid azure login'() { given: - def REGISTRY = 'seqeralabs.azurecr.io' + def registry = 'seqeralabs.azurecr.io' expect: - loginService.login(REGISTRY, azureUsername, azurePassword) + loginService.login(registry, azureUsername, azurePassword) } @IgnoreIf({!System.getenv('AWS_ACCESS_KEY_ID')}) void 'test valid aws ecr private'() { given: - String REGISTRY = '195996028523.dkr.ecr.eu-west-1.amazonaws.com' + String registry = '195996028523.dkr.ecr.eu-west-1.amazonaws.com' expect: - loginService.login(REGISTRY, awsEcrUsername, awsEcrPassword) + loginService.login(registry, awsEcrUsername, awsEcrPassword) } @IgnoreIf({!System.getenv('AWS_ACCESS_KEY_ID')}) void 'test valid aws ecr public'() { given: - String REGISTRY = 'public.ecr.aws' + String registry = 'public.ecr.aws' expect: - loginService.login(REGISTRY, awsEcrUsername, awsEcrPassword) + loginService.login(registry, awsEcrUsername, awsEcrPassword) } void 'test containerService valid login'() { From d0709aafe307248d4a4d38821359dd3523f9fd27 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Fri, 30 Aug 2024 11:34:27 +0200 Subject: [PATCH 17/18] fixed errors Signed-off-by: munishchouhan --- .../io/seqera/wave/service/CredentialServiceImpl.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy index 2ac43038e..351428b6d 100644 --- a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy @@ -81,12 +81,12 @@ class CredentialServiceImpl implements CredentialsService { // This cannot be implemented at the moment since, in tower, container registry // credentials are associated to the whole registry final repo = container.repository ?: DOCKER_IO - final creds = findBestMatchingCreds(repo, all) - if (!creds && identity.workflowId && AwsEcrService.isEcrHost(registryName) ) { + 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 } From f412a8f008a5eecbb53cdfd85f8f8497c930625c Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Fri, 30 Aug 2024 15:17:47 +0200 Subject: [PATCH 18/18] fixed tests Signed-off-by: munishchouhan --- .../groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy | 2 +- .../io/seqera/wave/service/CredentialsServiceTest.groovy | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy index 69f759496..9b166f1b1 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy @@ -204,7 +204,7 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis def c5 = new RegistryAuthServiceImpl.CacheKey(i1, a1, k3) expect: - c1.stableKey() == '23476a51c7b6216a' + c1.stableKey() == 'c234dc4c210c6612' c1.stableKey() == c2.stableKey() c1.stableKey() == c3.stableKey() and: diff --git a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy index 365204eaa..e3ddba736 100644 --- a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy @@ -29,6 +29,7 @@ import java.util.concurrent.CompletableFuture import io.micronaut.test.annotation.MockBean import io.micronaut.test.extensions.spock.annotation.MicronautTest import io.seqera.tower.crypto.AsymmetricCipher +import io.seqera.wave.core.ContainerPath import io.seqera.wave.model.ContainerCoordinates import io.seqera.wave.service.pairing.PairingRecord import io.seqera.wave.service.pairing.PairingService @@ -313,9 +314,10 @@ class CredentialsServiceTest extends Specification { and: def identity = new PlatformId(new User(id:userId), workspaceId,token,towerEndpoint,workflowId) def auth = JwtAuth.of(identity) + def containerPath = ContainerCoordinates.parse("$registryName/foo") when: 'look those registry credentials from tower' - def containerCredentials = credentialsService.findRegistryCreds(registryName,identity) + def containerCredentials = credentialsService.findRegistryCreds(containerPath, identity) then: 'the registered key is fetched correctly from the security service' 1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, towerEndpoint) >> keyRecord