|
| 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