From 1ae4eeecb669052b14830e52f6330d8930c4f1f8 Mon Sep 17 00:00:00 2001 From: Josh Perry Date: Tue, 24 Aug 2021 11:59:23 -0600 Subject: [PATCH 1/5] Add gitlabcijwtauth plugin --- security/gitlabCiJwtAuth/README.md | 43 +++++++ .../gitlabCiJwtAuth/gitlabCiJwtAuth.groovy | 112 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 security/gitlabCiJwtAuth/README.md create mode 100644 security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy diff --git a/security/gitlabCiJwtAuth/README.md b/security/gitlabCiJwtAuth/README.md new file mode 100644 index 00000000..62ce8756 --- /dev/null +++ b/security/gitlabCiJwtAuth/README.md @@ -0,0 +1,43 @@ +This User Plugin adds an authentication realm that can validate gitlab CI job jwt tokens +as an auth credential. The jwt token must be sent with a `{JWT}` prefix or artifactory +will recognize it as its own jwt and intercept the login attempt before this plugin sees it. + +This effectively gives gitlab projects themselves an identity in artifactory's eyes by +leveraging signed metadata in the JWT token, and allows permissions to be dynamically attached +to requests from those identities without needing to create discrete users or credentials. + +Once a JWT auth request is validated, the plugin will then look for a group in artifactory's +DB with a `gitlab-` prefix and a postfix matching the name of each level of the `project_path` +claim's hierarchy (with any specified root node removed and all characthers lowercased). + +For example, with a `project_path: 'org/platform/k8s'` it would look for groups named +`gitlab-platform` and `gitlab-platform-k8s`, attaching any it finds to the request session. +The session would then continue with any permissions assigned to the attached groups. + +If no groups are found, then the request continues with the context of an anonymous user. + +An example of authenticating using the gitlab job jwt token for pushing docker images from +the `.gitlabci.yml` file: + +``` +stages: + - build +variables: + REGISTRY: docker-local.artifactory.example.com + REGISTRY_IMAGE: ${REGISTRY}/platform/k8s/myimage + ARTIFACTORY_URL: "https://docker-local.artifactory.example.com/" + +build: + stage: build + image: docker:latest + before_script: + - docker login -u gitlabci -p "{JWT}${CI_JOB_JWT}" $REGISTRY + script: + - docker build --tag $REGISTRY_IMAGE:$CI_COMMIT_SHA . + - docker push $REGISTRY_IMAGE:$CI_COMMIT_SHA +``` + +This would be paired with a group in artifactory that has a write permission to the `docker-local` +repo at path `platform/k8s/**` to limit this particular repo to push to a unique registry path. + +jfrog plugin docs: https://www.jfrog.com/confluence/display/JFROG/User+Plugins#UserPlugins-Realms diff --git a/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy b/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy new file mode 100644 index 00000000..3bfb0fa3 --- /dev/null +++ b/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 MX Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@Grab(group='com.auth0', module='java-jwt', version='3.18.1') +@Grab(group='com.auth0', module='jwks-rsa', version='0.19.0') +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.exceptions.JWTVerificationException +import com.auth0.jwk.UrlJwkProvider + +import org.artifactory.api.security.UserGroupService + +import java.security.interfaces.RSAPublicKey + +/** + * + * This plugin allows gitlab projects to authenticate directly with artifactory + * using job-specific signed JWT tokens and the claims they contain to drive + * dynamic autorization policies without the need to manage users and credentials + * between gitlab and artifactory. + * + * @author Josh Perry + * @since 08/24/2021 + */ + +@Field final String ISSUER = 'gitlab.example.com' +@Field final String IGNORE_ROOT = 'org/' + +realms { + + gitlabJwtRealm(autoCreateUsers: false) { + + authenticate { username, credentials -> + try { + // Only handle requests with the virtual user `gitlabci` + if (username != 'gitlabci') return false + + // Check if this is a gitlab JWT credential + if (!credentials.startsWith('{JWT}')) return false + def realjwt = credentials.drop(5) + + log.info('got gitlabci JWT auth request') + + // Pre-decode the (currently) untrusted token + def prejwt = JWT.decode(realjwt) + + log.debug('loading jwks provider') + // Setup the jwks validation with the key ID from the token + def provider = new UrlJwkProvider(new URL("https://$ISSUER/-/jwks")) + def jwk = provider.get(prejwt.getKeyId()) + def algo = Algorithm.RSA256((RSAPublicKey)jwk.getPublicKey(), null) + + log.debug('creating verifier') + // Create the verification policy + def verifier = JWT.require(algo) + .withIssuer(ISSUER) + .build() + + log.debug('verifying token') + // Verify and get a trusted decoded token + def jwt = verifier.verify(realjwt) + + // Get the project path without a particular root + def path = jwt.getClaim('project_path').asString().toLowerCase() + if (path.startsWith(IGNORE_ROOT)) + path = path.drop(IGNORE_ROOT.length()) + log.debug('got project path claim {}', path) + + // Find and attach any `gitlab-` groups for each level of the project path + def paths = path.split('/') + def groupsvc = ctx.beanForType(UserGroupService.class) + asSystem { + for(x in (0..paths.size()-1)) { + def groupname = "gitlab-${paths[0..x].join('-')}" + + log.debug('checking for group {}', groupname) + if(groupsvc.findGroup(groupname) != null) { + log.debug('attaching group {}', groupname) + groups += groupname + } + } + } + + return true + } catch(JWTVerificationException e) { + log.error('Error verifying jwt signature') + } catch(Exception e) { + log.error("Unexpected Error: ${e}") + } + + return false + } + + userExists { username -> + return username == 'gitlabci' + } + + } + +} From a02efa55a08c5d6cf6424fff06993d2906e914b8 Mon Sep 17 00:00:00 2001 From: Josh Perry Date: Tue, 24 Aug 2021 12:04:44 -0600 Subject: [PATCH 2/5] Add failing test --- security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy diff --git a/security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy b/security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy new file mode 100644 index 00000000..88f56471 --- /dev/null +++ b/security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy @@ -0,0 +1,10 @@ +import spock.lang.Specification + +class PluginTest extends Specification { + def 'not implemented plugin test'() { + when: + throw new Exception("Not implemented.") + then: + false + } +} From 62c51f7b38de4e3ae06f7071ab6dd9cf8f98cd60 Mon Sep 17 00:00:00 2001 From: Josh Perry Date: Tue, 24 Aug 2021 12:04:55 -0600 Subject: [PATCH 3/5] Make group info more explicit in docs --- security/gitlabCiJwtAuth/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/security/gitlabCiJwtAuth/README.md b/security/gitlabCiJwtAuth/README.md index 62ce8756..db3ba6b6 100644 --- a/security/gitlabCiJwtAuth/README.md +++ b/security/gitlabCiJwtAuth/README.md @@ -37,7 +37,8 @@ build: - docker push $REGISTRY_IMAGE:$CI_COMMIT_SHA ``` -This would be paired with a group in artifactory that has a write permission to the `docker-local` -repo at path `platform/k8s/**` to limit this particular repo to push to a unique registry path. +This would be paired with a group named `gitlab-platform-k8s` in artifactory that has a write +permission to the `docker-local` repo at path `platform/k8s/**` to limit this particular repo to +push to a unique registry path. jfrog plugin docs: https://www.jfrog.com/confluence/display/JFROG/User+Plugins#UserPlugins-Realms From f68b5e1079104fc54ee9a2f8f0b523a7399661a1 Mon Sep 17 00:00:00 2001 From: Josh Perry Date: Tue, 24 Aug 2021 12:25:30 -0600 Subject: [PATCH 4/5] Fix test case and class name --- .../{gitlabCiJwtAuthTest.groovy => GitlabCiJwtAuthTest.groovy} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename security/gitlabCiJwtAuth/{gitlabCiJwtAuthTest.groovy => GitlabCiJwtAuthTest.groovy} (77%) diff --git a/security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy b/security/gitlabCiJwtAuth/GitlabCiJwtAuthTest.groovy similarity index 77% rename from security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy rename to security/gitlabCiJwtAuth/GitlabCiJwtAuthTest.groovy index 88f56471..209fb5ca 100644 --- a/security/gitlabCiJwtAuth/gitlabCiJwtAuthTest.groovy +++ b/security/gitlabCiJwtAuth/GitlabCiJwtAuthTest.groovy @@ -1,6 +1,6 @@ import spock.lang.Specification -class PluginTest extends Specification { +class GitlabCiJwtAuthTest extends Specification { def 'not implemented plugin test'() { when: throw new Exception("Not implemented.") From bd71c3159e2b1d34c941ee2d2838ac30dce5f181 Mon Sep 17 00:00:00 2001 From: Josh Perry Date: Tue, 24 Aug 2021 12:27:17 -0600 Subject: [PATCH 5/5] delint --- security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy b/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy index 3bfb0fa3..d6ea22f7 100644 --- a/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy +++ b/security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy @@ -28,7 +28,7 @@ import java.security.interfaces.RSAPublicKey * * This plugin allows gitlab projects to authenticate directly with artifactory * using job-specific signed JWT tokens and the claims they contain to drive - * dynamic autorization policies without the need to manage users and credentials + * dynamic authorization policies without the need to manage users and credentials * between gitlab and artifactory. * * @author Josh Perry