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

Add gitlabcijwtauth plugin #394

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions security/gitlabCiJwtAuth/GitlabCiJwtAuthTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import spock.lang.Specification

class GitlabCiJwtAuthTest extends Specification {
def 'not implemented plugin test'() {
when:
throw new Exception("Not implemented.")
then:
false
}
}
44 changes: 44 additions & 0 deletions security/gitlabCiJwtAuth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 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
112 changes: 112 additions & 0 deletions security/gitlabCiJwtAuth/gitlabCiJwtAuth.groovy
Original file line number Diff line number Diff line change
@@ -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 authorization policies without the need to manage users and credentials
* between gitlab and artifactory.
*
* @author <a href="mailto:[email protected]">Josh Perry</a>
* @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'
}

}

}