Skip to content

Commit 1ae4eee

Browse files
committed
Add gitlabcijwtauth plugin
1 parent 31d2423 commit 1ae4eee

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

security/gitlabCiJwtAuth/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
This User Plugin adds an authentication realm that can validate gitlab CI job jwt tokens
2+
as an auth credential. The jwt token must be sent with a `{JWT}` prefix or artifactory
3+
will recognize it as its own jwt and intercept the login attempt before this plugin sees it.
4+
5+
This effectively gives gitlab projects themselves an identity in artifactory's eyes by
6+
leveraging signed metadata in the JWT token, and allows permissions to be dynamically attached
7+
to requests from those identities without needing to create discrete users or credentials.
8+
9+
Once a JWT auth request is validated, the plugin will then look for a group in artifactory's
10+
DB with a `gitlab-` prefix and a postfix matching the name of each level of the `project_path`
11+
claim's hierarchy (with any specified root node removed and all characthers lowercased).
12+
13+
For example, with a `project_path: 'org/platform/k8s'` it would look for groups named
14+
`gitlab-platform` and `gitlab-platform-k8s`, attaching any it finds to the request session.
15+
The session would then continue with any permissions assigned to the attached groups.
16+
17+
If no groups are found, then the request continues with the context of an anonymous user.
18+
19+
An example of authenticating using the gitlab job jwt token for pushing docker images from
20+
the `.gitlabci.yml` file:
21+
22+
```
23+
stages:
24+
- build
25+
variables:
26+
REGISTRY: docker-local.artifactory.example.com
27+
REGISTRY_IMAGE: ${REGISTRY}/platform/k8s/myimage
28+
ARTIFACTORY_URL: "https://docker-local.artifactory.example.com/"
29+
30+
build:
31+
stage: build
32+
image: docker:latest
33+
before_script:
34+
- docker login -u gitlabci -p "{JWT}${CI_JOB_JWT}" $REGISTRY
35+
script:
36+
- docker build --tag $REGISTRY_IMAGE:$CI_COMMIT_SHA .
37+
- docker push $REGISTRY_IMAGE:$CI_COMMIT_SHA
38+
```
39+
40+
This would be paired with a group in artifactory that has a write permission to the `docker-local`
41+
repo at path `platform/k8s/**` to limit this particular repo to push to a unique registry path.
42+
43+
jfrog plugin docs: https://www.jfrog.com/confluence/display/JFROG/User+Plugins#UserPlugins-Realms
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (C) 2021 MX Technologies, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@Grab(group='com.auth0', module='java-jwt', version='3.18.1')
17+
@Grab(group='com.auth0', module='jwks-rsa', version='0.19.0')
18+
import com.auth0.jwt.JWT
19+
import com.auth0.jwt.algorithms.Algorithm
20+
import com.auth0.jwt.exceptions.JWTVerificationException
21+
import com.auth0.jwk.UrlJwkProvider
22+
23+
import org.artifactory.api.security.UserGroupService
24+
25+
import java.security.interfaces.RSAPublicKey
26+
27+
/**
28+
*
29+
* This plugin allows gitlab projects to authenticate directly with artifactory
30+
* using job-specific signed JWT tokens and the claims they contain to drive
31+
* dynamic autorization policies without the need to manage users and credentials
32+
* between gitlab and artifactory.
33+
*
34+
* @author <a href="mailto:[email protected]">Josh Perry</a>
35+
* @since 08/24/2021
36+
*/
37+
38+
@Field final String ISSUER = 'gitlab.example.com'
39+
@Field final String IGNORE_ROOT = 'org/'
40+
41+
realms {
42+
43+
gitlabJwtRealm(autoCreateUsers: false) {
44+
45+
authenticate { username, credentials ->
46+
try {
47+
// Only handle requests with the virtual user `gitlabci`
48+
if (username != 'gitlabci') return false
49+
50+
// Check if this is a gitlab JWT credential
51+
if (!credentials.startsWith('{JWT}')) return false
52+
def realjwt = credentials.drop(5)
53+
54+
log.info('got gitlabci JWT auth request')
55+
56+
// Pre-decode the (currently) untrusted token
57+
def prejwt = JWT.decode(realjwt)
58+
59+
log.debug('loading jwks provider')
60+
// Setup the jwks validation with the key ID from the token
61+
def provider = new UrlJwkProvider(new URL("https://$ISSUER/-/jwks"))
62+
def jwk = provider.get(prejwt.getKeyId())
63+
def algo = Algorithm.RSA256((RSAPublicKey)jwk.getPublicKey(), null)
64+
65+
log.debug('creating verifier')
66+
// Create the verification policy
67+
def verifier = JWT.require(algo)
68+
.withIssuer(ISSUER)
69+
.build()
70+
71+
log.debug('verifying token')
72+
// Verify and get a trusted decoded token
73+
def jwt = verifier.verify(realjwt)
74+
75+
// Get the project path without a particular root
76+
def path = jwt.getClaim('project_path').asString().toLowerCase()
77+
if (path.startsWith(IGNORE_ROOT))
78+
path = path.drop(IGNORE_ROOT.length())
79+
log.debug('got project path claim {}', path)
80+
81+
// Find and attach any `gitlab-` groups for each level of the project path
82+
def paths = path.split('/')
83+
def groupsvc = ctx.beanForType(UserGroupService.class)
84+
asSystem {
85+
for(x in (0..paths.size()-1)) {
86+
def groupname = "gitlab-${paths[0..x].join('-')}"
87+
88+
log.debug('checking for group {}', groupname)
89+
if(groupsvc.findGroup(groupname) != null) {
90+
log.debug('attaching group {}', groupname)
91+
groups += groupname
92+
}
93+
}
94+
}
95+
96+
return true
97+
} catch(JWTVerificationException e) {
98+
log.error('Error verifying jwt signature')
99+
} catch(Exception e) {
100+
log.error("Unexpected Error: ${e}")
101+
}
102+
103+
return false
104+
}
105+
106+
userExists { username ->
107+
return username == 'gitlabci'
108+
}
109+
110+
}
111+
112+
}

0 commit comments

Comments
 (0)