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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
/*
 * Copyright 2013 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.manager;
 
import java.io.File;
import java.io.FileFilter;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
import javax.servlet.http.HttpServletRequest;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.gitblit.Constants;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.FederationSet;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.Base64;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Singleton;
 
/**
 * Federation manager controls all aspects of handling federation sets, tokens,
 * and proposals.
 *
 * @author James Moger
 *
 */
@Singleton
public class FederationManager implements IFederationManager {
 
    private final Logger logger = LoggerFactory.getLogger(getClass());
 
    private final List<FederationModel> federationRegistrations = Collections
            .synchronizedList(new ArrayList<FederationModel>());
 
    private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
 
    private final IStoredSettings settings;
 
    private final IRuntimeManager runtimeManager;
 
    private final INotificationManager notificationManager;
 
    private final IRepositoryManager repositoryManager;
 
    @Inject
    public FederationManager(
            IRuntimeManager runtimeManager,
            INotificationManager notificationManager,
            IRepositoryManager repositoryManager) {
 
        this.settings = runtimeManager.getSettings();
        this.runtimeManager = runtimeManager;
        this.notificationManager = notificationManager;
        this.repositoryManager = repositoryManager;
    }
 
    @Override
    public FederationManager start() {
        return this;
    }
 
    @Override
    public FederationManager stop() {
        return this;
    }
 
    /**
     * Returns the path of the proposals folder. This method checks to see if
     * Gitblit is running on a cloud service and may return an adjusted path.
     *
     * @return the proposals folder path
     */
    @Override
    public File getProposalsFolder() {
        return runtimeManager.getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals");
    }
 
    @Override
    public boolean canFederate() {
        String passphrase = settings.getString(Keys.federation.passphrase, "");
        return !StringUtils.isEmpty(passphrase);
    }
 
    /**
     * Returns the federation user account.
     *
     * @return the federation user account
     */
    @Override
    public UserModel getFederationUser() {
        // the federation user is an administrator
        UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
        federationUser.canAdmin = true;
        return federationUser;
    }
 
    @Override
    public UserModel authenticate(HttpServletRequest httpRequest) {
        if (canFederate()) {
            // try to authenticate federation user for cloning
            final String authorization = httpRequest.getHeader("Authorization");
            if (authorization != null && authorization.startsWith("Basic")) {
                // Authorization: Basic base64credentials
                String base64Credentials = authorization.substring("Basic".length()).trim();
                String credentials = new String(Base64.decode(base64Credentials),
                        Charset.forName("UTF-8"));
                // credentials = username:password
                final String[] values = credentials.split(":", 2);
                if (values.length == 2) {
                    String username = StringUtils.decodeUsername(values[0]);
                    String password = values[1];
                    if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
                        List<String> tokens = getFederationTokens();
                        if (tokens.contains(password)) {
                            return getFederationUser();
                        }
                    }
                }
            }
        }
        return null;
    }
 
    /**
     * Returns the list of federated gitblit instances that this instance will
     * try to pull.
     *
     * @return list of registered gitblit instances
     */
    @Override
    public List<FederationModel> getFederationRegistrations() {
        if (federationRegistrations.isEmpty()) {
            federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
        }
        return federationRegistrations;
    }
 
    /**
     * Retrieve the specified federation registration.
     *
     * @param name
     *            the name of the registration
     * @return a federation registration
     */
    @Override
    public FederationModel getFederationRegistration(String url, String name) {
        // check registrations
        for (FederationModel r : getFederationRegistrations()) {
            if (r.name.equals(name) && r.url.equals(url)) {
                return r;
            }
        }
 
        // check the results
        for (FederationModel r : getFederationResultRegistrations()) {
            if (r.name.equals(name) && r.url.equals(url)) {
                return r;
            }
        }
        return null;
    }
 
    /**
     * Returns the list of federation sets.
     *
     * @return list of federation sets
     */
    @Override
    public List<FederationSet> getFederationSets(String gitblitUrl) {
        List<FederationSet> list = new ArrayList<FederationSet>();
        // generate standard tokens
        for (FederationToken type : FederationToken.values()) {
            FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
            fset.repositories = getRepositories(gitblitUrl, fset.token);
            list.add(fset);
        }
        // generate tokens for federation sets
        for (String set : settings.getStrings(Keys.federation.sets)) {
            FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
                    getFederationToken(set));
            fset.repositories = getRepositories(gitblitUrl, fset.token);
            list.add(fset);
        }
        return list;
    }
 
    /**
     * Returns the list of possible federation tokens for this Gitblit instance.
     *
     * @return list of federation tokens
     */
    @Override
    public List<String> getFederationTokens() {
        List<String> tokens = new ArrayList<String>();
        // generate standard tokens
        for (FederationToken type : FederationToken.values()) {
            tokens.add(getFederationToken(type));
        }
        // generate tokens for federation sets
        for (String set : settings.getStrings(Keys.federation.sets)) {
            tokens.add(getFederationToken(set));
        }
        return tokens;
    }
 
    /**
     * Returns the specified federation token for this Gitblit instance.
     *
     * @param type
     * @return a federation token
     */
    @Override
    public String getFederationToken(FederationToken type) {
        return getFederationToken(type.name());
    }
 
    /**
     * Returns the specified federation token for this Gitblit instance.
     *
     * @param value
     * @return a federation token
     */
    @Override
    public String getFederationToken(String value) {
        String passphrase = settings.getString(Keys.federation.passphrase, "");
        return StringUtils.getSHA1(passphrase + "-" + value);
    }
 
    /**
     * Compares the provided token with this Gitblit instance's tokens and
     * determines if the requested permission may be granted to the token.
     *
     * @param req
     * @param token
     * @return true if the request can be executed
     */
    @Override
    public boolean validateFederationRequest(FederationRequest req, String token) {
        String all = getFederationToken(FederationToken.ALL);
        String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
        String jur = getFederationToken(FederationToken.REPOSITORIES);
        switch (req) {
        case PULL_REPOSITORIES:
            return token.equals(all) || token.equals(unr) || token.equals(jur);
        case PULL_USERS:
        case PULL_TEAMS:
            return token.equals(all) || token.equals(unr);
        case PULL_SETTINGS:
        case PULL_SCRIPTS:
            return token.equals(all);
        default:
            break;
        }
        return false;
    }
 
    /**
     * Acknowledge and cache the status of a remote Gitblit instance.
     *
     * @param identification
     *            the identification of the pulling Gitblit instance
     * @param registration
     *            the registration from the pulling Gitblit instance
     * @return true if acknowledged
     */
    @Override
    public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
        // reset the url to the identification of the pulling Gitblit instance
        registration.url = identification;
        String id = identification;
        if (!StringUtils.isEmpty(registration.folder)) {
            id += "-" + registration.folder;
        }
        federationPullResults.put(id, registration);
        return true;
    }
 
    /**
     * Returns the list of registration results.
     *
     * @return the list of registration results
     */
    @Override
    public List<FederationModel> getFederationResultRegistrations() {
        return new ArrayList<FederationModel>(federationPullResults.values());
    }
 
    /**
     * Submit a federation proposal. The proposal is cached locally and the
     * Gitblit administrator(s) are notified via email.
     *
     * @param proposal
     *            the proposal
     * @param gitblitUrl
     *            the url of your gitblit instance to send an email to
     *            administrators
     * @return true if the proposal was submitted
     */
    @Override
    public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
        // convert proposal to json
        String json = JsonUtils.toJsonString(proposal);
 
        try {
            // make the proposals folder
            File proposalsFolder = getProposalsFolder();
            proposalsFolder.mkdirs();
 
            // cache json to a file
            File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
            com.gitblit.utils.FileUtils.writeContent(file, json);
        } catch (Exception e) {
            logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
        }
 
        // send an email, if possible
        notificationManager.sendMailToAdministrators("Federation proposal from " + proposal.url,
                "Please review the proposal @ " + gitblitUrl + "/proposal/" + proposal.token);
        return true;
    }
 
    /**
     * Returns the list of pending federation proposals
     *
     * @return list of federation proposals
     */
    @Override
    public List<FederationProposal> getPendingFederationProposals() {
        List<FederationProposal> list = new ArrayList<FederationProposal>();
        File folder = getProposalsFolder();
        if (folder.exists()) {
            File[] files = folder.listFiles(new FileFilter() {
                @Override
                public boolean accept(File file) {
                    return file.isFile()
                            && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
                }
            });
            for (File file : files) {
                String json = com.gitblit.utils.FileUtils.readContent(file, null);
                FederationProposal proposal = JsonUtils.fromJsonString(json,
                        FederationProposal.class);
                list.add(proposal);
            }
        }
        return list;
    }
 
    /**
     * Get repositories for the specified token.
     *
     * @param gitblitUrl
     *            the base url of this gitblit instance
     * @param token
     *            the federation token
     * @return a map of <cloneurl, RepositoryModel>
     */
    @Override
    public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
        Map<String, String> federationSets = new HashMap<String, String>();
        for (String set : settings.getStrings(Keys.federation.sets)) {
            federationSets.put(getFederationToken(set), set);
        }
 
        // Determine the Gitblit clone url
        StringBuilder sb = new StringBuilder();
        sb.append(gitblitUrl);
        sb.append(Constants.R_PATH);
        sb.append("{0}");
        String cloneUrl = sb.toString();
 
        // Retrieve all available repositories
        UserModel user = getFederationUser();
        List<RepositoryModel> list = repositoryManager.getRepositoryModels(user);
 
        // create the [cloneurl, repositoryModel] map
        Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
        for (RepositoryModel model : list) {
            // by default, setup the url for THIS repository
            String url = MessageFormat.format(cloneUrl, model.name);
            switch (model.federationStrategy) {
            case EXCLUDE:
                // skip this repository
                continue;
            case FEDERATE_ORIGIN:
                // federate the origin, if it is defined
                if (!StringUtils.isEmpty(model.origin)) {
                    url = model.origin;
                }
                break;
            default:
                break;
            }
 
            if (federationSets.containsKey(token)) {
                // include repositories only for federation set
                String set = federationSets.get(token);
                if (model.federationSets.contains(set)) {
                    repositories.put(url, model);
                }
            } else {
                // standard federation token for ALL
                repositories.put(url, model);
            }
        }
        return repositories;
    }
 
    /**
     * Creates a proposal from the token.
     *
     * @param gitblitUrl
     *            the url of this Gitblit instance
     * @param token
     * @return a potential proposal
     */
    @Override
    public FederationProposal createFederationProposal(String gitblitUrl, String token) {
        FederationToken tokenType = FederationToken.REPOSITORIES;
        for (FederationToken type : FederationToken.values()) {
            if (token.equals(getFederationToken(type))) {
                tokenType = type;
                break;
            }
        }
        Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
        FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
                repositories);
        return proposal;
    }
 
    /**
     * Returns the proposal identified by the supplied token.
     *
     * @param token
     * @return the specified proposal or null
     */
    @Override
    public FederationProposal getPendingFederationProposal(String token) {
        List<FederationProposal> list = getPendingFederationProposals();
        for (FederationProposal proposal : list) {
            if (proposal.token.equals(token)) {
                return proposal;
            }
        }
        return null;
    }
 
    /**
     * Deletes a pending federation proposal.
     *
     * @param a
     *            proposal
     * @return true if the proposal was deleted
     */
    @Override
    public boolean deletePendingFederationProposal(FederationProposal proposal) {
        File folder = getProposalsFolder();
        File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
        return file.delete();
    }
}