James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
/*
 * Copyright 2011 gitblit.com.
 *
 * 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.
 */
package com.gitblit.servlet;
 
import java.text.MessageFormat;
 
import com.google.inject.Inject;
import com.google.inject.Singleton;
 
import javax.servlet.http.HttpServletRequest;
 
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.GitBlitException;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
 
/**
 * The GitFilter is an AccessRestrictionFilter which ensures that Git client
 * requests for push, clone, or view restricted repositories are authenticated
 * and authorized.
 *
 * @author James Moger
 *
 */
@Singleton
public class GitFilter extends AccessRestrictionFilter {
 
    protected static final String gitReceivePack = "/git-receive-pack";
 
    protected static final String gitUploadPack = "/git-upload-pack";
    
    protected static final String gitLfs = "/info/lfs";
    
    protected static final String[] suffixes = { gitReceivePack, gitUploadPack, "/info/refs", "/HEAD",
            "/objects", gitLfs };
 
    private IStoredSettings settings;
 
    private IFederationManager federationManager;
 
    @Inject
    public GitFilter(
            IStoredSettings settings,
            IRuntimeManager runtimeManager,
            IAuthenticationManager authenticationManager,
            IRepositoryManager repositoryManager,
            IFederationManager federationManager) {
 
        super(runtimeManager, authenticationManager, repositoryManager);
 
        this.settings = settings;
        this.federationManager = federationManager;
    }
 
    /**
     * Extract the repository name from the url.
     *
     * @param cloneUrl
     * @return repository name
     */
    public static String getRepositoryName(String value) {
        String repository = value;
        // get the repository name from the url by finding a known url suffix
        for (String urlSuffix : suffixes) {
            if (repository.indexOf(urlSuffix) > -1) {
                repository = repository.substring(0, repository.indexOf(urlSuffix));
            }
        }
        return repository;
    }
 
    /**
     * Extract the repository name from the url.
     *
     * @param url
     * @return repository name
     */
    @Override
    protected String extractRepositoryName(String url) {
        return GitFilter.getRepositoryName(url);
    }
 
    /**
     * Analyze the url and returns the action of the request. Return values are
     * either "/git-receive-pack" or "/git-upload-pack".
     *
     * @param serverUrl
     * @return action of the request
     */
    @Override
    protected String getUrlRequestAction(String suffix) {
        if (!StringUtils.isEmpty(suffix)) {
            if (suffix.startsWith(gitReceivePack)) {
                return gitReceivePack;
            } else if (suffix.startsWith(gitUploadPack)) {
                return gitUploadPack;
            } else if (suffix.contains("?service=git-receive-pack")) {
                return gitReceivePack;
            } else if (suffix.contains("?service=git-upload-pack")) {
                return gitUploadPack;
            } else if (suffix.startsWith(gitLfs)) {
                return gitLfs;
            } else {
                return gitUploadPack;
            }
        }
        return null;
    }
 
    /**
     * Returns the user making the request, if the user has authenticated.
     *
     * @param httpRequest
     * @return user
     */
    @Override
    protected UserModel getUser(HttpServletRequest httpRequest) {
        UserModel user = authenticationManager.authenticate(httpRequest, requiresClientCertificate());
        if (user == null) {
            user = federationManager.authenticate(httpRequest);
        }
        return user;
    }
 
    /**
     * Determine if a non-existing repository can be created using this filter.
     *
     * @return true if the server allows repository creation on-push
     */
    @Override
    protected boolean isCreationAllowed(String action) {
        
        //Repository must already exist before large files can be deposited
        if (action.equals(gitLfs)) {
            return false;
        }
        
        return settings.getBoolean(Keys.git.allowCreateOnPush, true);
    }
 
    /**
     * Determine if the repository can receive pushes.
     *
     * @param repository
     * @param action
     * @return true if the action may be performed
     */
    @Override
    protected boolean isActionAllowed(RepositoryModel repository, String action, String method) {
        // the log here has been moved into ReceiveHook to provide clients with
        // error messages
        if (gitLfs.equals(action)) {
            if (!method.matches("GET|POST|PUT|HEAD")) {
                return false;
            }
        }
        
        return true;
    }
 
    @Override
    protected boolean requiresClientCertificate() {
        return settings.getBoolean(Keys.git.requiresClientCertificate, false);
    }
 
    /**
     * Determine if the repository requires authentication.
     *
     * @param repository
     * @param action
     * @param method
     * @return true if authentication required
     */
    @Override
    protected boolean requiresAuthentication(RepositoryModel repository, String action, String method) {
        if (gitUploadPack.equals(action)) {
            // send to client
            return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE);
        } else if (gitReceivePack.equals(action)) {
            // receive from client
            return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH);
        } else if (gitLfs.equals(action)) {
            
            if (method.matches("GET|HEAD")) {
                return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE);
            } else {
                //NOTE: Treat POST as PUT as as without reading message type cannot determine 
                return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH);
            }
        }
        return false;
    }
 
    /**
     * Determine if the user can access the repository and perform the specified
     * action.
     *
     * @param repository
     * @param user
     * @param action
     * @return true if user may execute the action on the repository
     */
    @Override
    protected boolean canAccess(RepositoryModel repository, UserModel user, String action) {
        if (!settings.getBoolean(Keys.git.enableGitServlet, true)) {
            // Git Servlet disabled
            return false;
        }
        if (action.equals(gitReceivePack)) {
            // push permissions are enforced in the receive pack
            return true;
        } else if (action.equals(gitUploadPack)) {
            // Clone request
            if (user.canClone(repository)) {
                return true;
            } else {
                // user is unauthorized to clone this repository
                logger.warn(MessageFormat.format("user {0} is not authorized to clone {1}",
                        user.username, repository));
                return false;
            }
        }
        return true;
    }
 
    /**
     * An authenticated user with the CREATE role can create a repository on
     * push.
     *
     * @param user
     * @param repository
     * @param action
     * @return the repository model, if it is created, null otherwise
     */
    @Override
    protected RepositoryModel createRepository(UserModel user, String repository, String action) {
        boolean isPush = !StringUtils.isEmpty(action) && gitReceivePack.equals(action);
        
        if (action.equals(gitLfs)) {
            //Repository must already exist for any filestore actions
            return null;
        }
        
        if (isPush) {
            if (user.canCreate(repository)) {
                // user is pushing to a new repository
                // validate name
                if (repository.startsWith("../")) {
                    logger.error(MessageFormat.format("Illegal relative path in repository name! {0}", repository));
                    return null;
                }
                if (repository.contains("/../")) {
                    logger.error(MessageFormat.format("Illegal relative path in repository name! {0}", repository));
                    return null;
                }
 
                // confirm valid characters in repository name
                Character c = StringUtils.findInvalidCharacter(repository);
                if (c != null) {
                    logger.error(MessageFormat.format("Invalid character '{0}' in repository name {1}!", c, repository));
                    return null;
                }
 
                // create repository
                RepositoryModel model = new RepositoryModel();
                model.name = repository;
                model.addOwner(user.username);
                model.projectPath = StringUtils.getFirstPathElement(repository);
                if (model.isUsersPersonalRepository(user.username)) {
                    // personal repository, default to private for user
                    model.authorizationControl = AuthorizationControl.NAMED;
                    model.accessRestriction = AccessRestrictionType.VIEW;
                } else {
                    // common repository, user default server settings
                    model.authorizationControl = AuthorizationControl.fromName(settings.getString(Keys.git.defaultAuthorizationControl, ""));
                    model.accessRestriction = AccessRestrictionType.fromName(settings.getString(Keys.git.defaultAccessRestriction, "PUSH"));
                }
 
                // create the repository
                try {
                    repositoryManager.updateRepositoryModel(model.name, model, true);
                    logger.info(MessageFormat.format("{0} created {1} ON-PUSH", user.username, model.name));
                    return repositoryManager.getRepositoryModel(model.name);
                } catch (GitBlitException e) {
                    logger.error(MessageFormat.format("{0} failed to create repository {1} ON-PUSH!", user.username, model.name), e);
                }
            } else {
                logger.warn(MessageFormat.format("{0} is not permitted to create repository {1} ON-PUSH!", user.username, repository));
            }
        }
 
        // repository could not be created or action was not a push
        return null;
    }
    
    /**
     * Git lfs action uses an alternative authentication header, 
     * 
     * @param action
     * @return
     */
    @Override
    protected String getAuthenticationHeader(String action) {
 
        if (action.equals(gitLfs)) {
            return "LFS-Authenticate";
        }
        
        return super.getAuthenticationHeader(action);
    }
    
    /**
     * Interrogates the request headers based on the action
     * @param action
     * @param request
     * @return
     */
    @Override
    protected boolean hasValidRequestHeader(String action,
            HttpServletRequest request) {
 
        if (action.equals(gitLfs) && request.getMethod().equals("POST")) {
            if (     !hasContentInRequestHeader(request, "Accept", FilestoreServlet.GIT_LFS_META_MIME)
                 || !hasContentInRequestHeader(request, "Content-Type", FilestoreServlet.GIT_LFS_META_MIME)) {
                return false;
            }                
        }
            
        return super.hasValidRequestHeader(action, request);
    }
}