This commit is contained in:
Mohamed Karray
2019-02-05 08:49:01 +01:00
198 changed files with 6827 additions and 2080 deletions

View File

@@ -0,0 +1,76 @@
# Clone empty repository
```http
GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1.
Accept-Encoding: identity.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1efk0qxy1dj5v133hev91zwsf4;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 130.
Server: Jetty(7.6.21.v20160908).
.
lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=bookmarks.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1rsxj8u1rq9wizawhyyxok2p5;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 0.
Server: Jetty(7.6.21.v20160908).
GET /scm/hg/hgtest?cmd=batch HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: cmds=heads+%3Bknown+nodes%3D.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=ewyx4m53d8dajjsob6gxobne;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 42.
Server: Jetty(7.6.21.v20160908).
0000000000000000000000000000000000000000
;
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1o0hou15jtiywsywutf30qwm8;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 05:57:18 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 15.
Server: Jetty(7.6.21.v20160908).
.
publishing.True
```

View File

@@ -0,0 +1,117 @@
# Push bookmark
```http
GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1.
Accept-Encoding: identity.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=7rq9vpp9svfm1sicq7h9vetmv;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 130.
Server: Jetty(7.6.21.v20160908).
lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
GET /scm/hg/hgtest?cmd=batch HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
T 172.17.0.2:8080 -> 172.17.0.1:36576 [AP]
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1553csz4sf7scyvw8mqnqfirn;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 43.
Server: Jetty(7.6.21.v20160908).
ef5993bb4abb32a0565c347844c6d939fc4f4b98
;1
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=11xa5u3nrmx8k1nar3sazg6jzh;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 15.
Server: Jetty(7.6.21.v20160908).
publishing.True
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=bookmarks.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1p1uzcvfe1pvzh2buzo658rxw;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 0.
Server: Jetty(7.6.21.v20160908).
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1mhlj3ucfzdp6ifmzoua4zwit;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 15.
Server: Jetty(7.6.21.v20160908).
publishing.True
POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: key=markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old=.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 0.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=s4vtagb303dv1xg809wnp7e8z;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 08:08:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 2.
Server: Jetty(7.6.21.v20160908).
.
1
```

View File

@@ -0,0 +1,167 @@
# Push multiple branches to new repository
```http
GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1.
Accept-Encoding: identity.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1wu06ykfd4bcv1uv731y4hss2m;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 130.
Server: Jetty(7.6.21.v20160908).
lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
GET /scm/hg/hgtest?cmd=batch HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1rajglvqx222g5nppcq3jdfk0;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 43.
Server: Jetty(7.6.21.v20160908).
0000000000000000000000000000000000000000
;0
GET /scm/hg/hgtest?cmd=known HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: nodes=c0ceccb3b2f0f5c977ff32b9337519e5f37942c2+187ddf37e237c370514487a0bb1a226f11a780b3+b5914611f84eae14543684b2721eec88b0edac12+8b63a323606f10c86b30465570c2574eb7a3a989.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=a5vykp1f0ga2186l8v3gu6lid;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 4.
Server: Jetty(7.6.21.v20160908).
0000
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=s8lpwqm4c2nqs9kwcg2ca6vm;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 15.
Server: Jetty(7.6.21.v20160908).
publishing.True
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=bookmarks.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1d2qj3kynxlhvk31oli4kk7vf;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 0.
Server: Jetty(7.6.21.v20160908).
POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: heads=686173686564+6768033e216468247bd031a0a2d9876d79818f8f.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 913.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HG10GZx...oh.U......E.1.....2q.<...s.1.YK*e#..b..{....{..%A.....
,\.....Y.XV....Q/J......`Q/.z.{...<.7....r.s.~?.?..<o....O.]....?.}..m?]z..I..u..}.a..rg..R[i.,D ...!.1..h.r.....G...M.\...J[.....+{.k...u..bL.!....F('..=Q.'......W.>5.~`..?..........O.j.0.....Ih.....!@.P... ..a
;!y..cT...]q.8Zg=...<..,.tq.*.........l........';..w^...w...-......Co..Fs.HYg...
9.F#.P......1..;......D.H.9$@.^....r:E..18...H....3..h...-.=.6l......=q .)."Yg..p\...s@.#.H.*....c8&96..2.GjJ.`.J....r...=Q1..@R.3.o{q...|.......yq.k..,cY..:[... ...S.2...VYp..c5..&.SFR.............V.d..o..........,.. A..M....k...0_.LO1..1"4.;...B....5.9.".U.m.e......]\../p..;?C..<vW.....|......F.8,....s....2.T
N. .k..>W9.........n.~o..gW...Q;..$....S..X.CN.5I].H..!.@...U..J...L.lY.../.-...6.:.Q.'...>.e'..<#3........OL}.52ra[..g*Y:Y....w...=..Z\...S.......tz..;..mf...W......&yUN.r.......4...........`..F...nT..U9................_.~..?...BwzUN.r....B.
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=163487i0ayf9s1k2ng9e1azadj;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 102.
Server: Jetty(7.6.21.v20160908).
1
adding changesets
adding manifests
adding file changes
added 5 changesets with 3 changes to 3 files
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=a3i712yjss6t1xsxltnssq0tl;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 58.
Server: Jetty(7.6.21.v20160908).
c0ceccb3b2f0f5c977ff32b9337519e5f37942c2.1
publishing.True
POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: key=ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 0.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=g8cavdze42d83knmuasrlg10;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 07:55:14 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 2.
Server: Jetty(7.6.21.v20160908).
.
1
```

View File

@@ -0,0 +1,183 @@
# Push multiple branches
```http
GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1.
Accept-Encoding: identity.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1mvm1rxg8333iib7754ksusxc;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 130.
Server: Jetty(7.6.21.v20160908).
lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
GET /scm/hg/hgtest?cmd=batch HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=58p9y9vcnz5cjs22dtw8mpwk;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 43.
Server: Jetty(7.6.21.v20160908).
c0ceccb3b2f0f5c977ff32b9337519e5f37942c2
;0
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=v5wfwj8k4t261dp6808cdouoa;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 15.
Server: Jetty(7.6.21.v20160908).
publishing.True
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=bookmarks.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=3pgqytfhm4za1dco9p41j9yz5;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 0.
Server: Jetty(7.6.21.v20160908).
GET /scm/hg/hgtest?cmd=branchmap HTTP/1.1.
Accept-Encoding: identity.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
.
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1tiz6zf7ui54e1j3d4vouxig5m;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 48.
Server: Jetty(7.6.21.v20160908).
default c0ceccb3b2f0f5c977ff32b9337519e5f37942c2
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=bookmarks.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1augu4tc71xax1dit20dtxzkez;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 0.
Server: Jetty(7.6.21.v20160908).
POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: heads=686173686564+95373ca7cd5371cb6c49bb755ee451d9ec585845.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 746.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HG10GZx...]H.Q...z..r.,.Y..Bw.~..c.Z&...hf.:......e.XK.X,...
,2.E1.B+...(.B"."*..z1.*......M...........93..k|..I..<...h..J_.L.9>.h..@.....op..^.....#....;.*..W....T@....!..dY....jT..A0O6.}..S.2..JPU.O6...aa...rY.VOf9.....7Ukj.&..<...z...j......%}..Jc.8c....k.."9.&".I.P.\..$.At......0..1..g.2.)<..$.. E..dn#....#.Y$3...n...5....J.e.......SNHN.q.MD..4..."I..`PF..?GH1..F..uES..Rl$47.....a........D.1...87.k.t..D..O_.3..6'cN.w.M..|@E.).X!.h*....U.B.X.....h..$.`4...
-..O.:./..oWN.....3...x.L......_[..../..k.R$.x.2..kkv.\2R....4...@.2...1Q..T
..(..m....s.Uo.......{.d.....Y....TYO...S.Pl`a5. ."N$.@...b...qJ.l.).n...1..F.Zy.....&>v;.q.....Jy..X.?.;....>U..|.....d.Y.*.q...NR.3...h.T..x..,.]...p{.^S.S...~..`..q.\j{.oCI.............K.....l9n.s......
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1e4fnqpncil9z1f7a2pya26nt7;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 102.
Server: Jetty(7.6.21.v20160908).
1
adding changesets
adding manifests
adding file changes
added 4 changesets with 2 changes to 2 files
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=f9hvrjssniym1qe33q0u8r2m8;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 101.
Server: Jetty(7.6.21.v20160908).
b5914611f84eae14543684b2721eec88b0edac12.1
187ddf37e237c370514487a0bb1a226f11a780b3.1
publishing.True
POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: key=ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 0.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=z5lrut6940a650sw6x9bls8a;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:16:50 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 2.
Server: Jetty(7.6.21.v20160908).
1
```

View File

@@ -0,0 +1,147 @@
# Push single changeset
```http
GET /scm/hg/hgtest?cmd=capabilities HTTP/1.1.
Accept-Encoding: identity.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=18r2i2jsba46d14ncsmcjdhaem;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 130.
Server: Jetty(7.6.21.v20160908).
lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
GET /scm/hg/hgtest?cmd=batch HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: cmds=heads+%3Bknown+nodes%3Dc0ceccb3b2f0f5c977ff32b9337519e5f37942c2.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=1fw0i0c5zpy281gfgha0f26git;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 43.
Server: Jetty(7.6.21.v20160908).
0000000000000000000000000000000000000000
;0
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=dfa46uaqgf39w3jhk857oymu;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 15.
Server: Jetty(7.6.21.v20160908).
publishing.True
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=bookmarks.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=2sk1llvrsagg33xgmwyirfpi;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 0.
Server: Jetty(7.6.21.v20160908).
POST /scm/hg/hgtest?cmd=unbundle HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: heads=686173686564+6768033e216468247bd031a0a2d9876d79818f8f.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 261.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HG10GZx.c``8w.....>|=Y..h.q.....N.......%......Z....&&&.&...YZ.&.&[$.........$.%q..&%..d&.).....%*.....Y.....9z...v\..FF......
..F..\.z%.%\\.)).)
.P[....D..[un..L).nc..q.m*.H.l#C...eZJ..YJ.Q.qR...e.aJ.EjjJ.AZ..A.Q..E.1.T.'D..C....7s.}..4G........3.S.mL.0.....zk
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=hlucs5utn1ifnpehqmjpt593;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 102.
Server: Jetty(7.6.21.v20160908).
1
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
T 172.17.0.1:33206 -> 172.17.0.2:8080 [AP]
GET /scm/hg/hgtest?cmd=listkeys HTTP/1.1.
Accept-Encoding: identity.
vary: X-HgArg-1.
x-hgarg-1: namespace=phases.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=15xomlrxl8qja1cj47rjpqda0y;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 58.
Server: Jetty(7.6.21.v20160908).
c0ceccb3b2f0f5c977ff32b9337519e5f37942c2.1
publishing.True
POST /scm/hg/hgtest?cmd=pushkey HTTP/1.1.
Accept-Encoding: identity.
content-type: application/mercurial-0.1.
vary: X-HgArg-1.
x-hgarg-1: key=c0ceccb3b2f0f5c977ff32b9337519e5f37942c2&namespace=phases&new=0&old=1.
accept: application/mercurial-0.1.
authorization: Basic c2NtYWRtaW46c2NtYWRtaW4=.
content-length: 0.
host: localhost:8080.
user-agent: mercurial/proto-1.0 (Mercurial 4.3.1).
HTTP/1.1 200 OK.
Set-Cookie: JSESSIONID=5zrop5v8e661ipk12tvru525;Path=/scm.
Expires: Thu, 01 Jan 1970 00:00:00 GMT.
Set-Cookie: rememberMe=deleteMe; Path=/scm; Max-Age=0; Expires=Wed, 28-Mar-2018 06:03:35 GMT.
Content-Type: application/mercurial-0.1.
Content-Length: 2.
Server: Jetty(7.6.21.v20160908).
1
```

161
pom.xml
View File

@@ -86,20 +86,6 @@
<url>http://maven.scm-manager.org/nexus/content/groups/public</url> <url>http://maven.scm-manager.org/nexus/content/groups/public</url>
</repository> </repository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</snapshots>
</repository>
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories> </repositories>
<pluginRepositories> <pluginRepositories>
@@ -174,18 +160,6 @@
<artifactId>assertj-core</artifactId> <artifactId>assertj-core</artifactId>
</dependency> </dependency>
<dependency>
<!-- Dependency used in Jenkinsfile. Including this in maven provides code completion in Jenkinsfile. -->
<groupId>com.github.cloudogu</groupId>
<artifactId>ces-build-lib</artifactId>
<!-- Keep this version in sync with the one used in Jenkinsfile -->
<version>9aadeeb</version>
<!-- Don't ship this dependency with the app -->
<optional>true</optional>
<!-- Don't inherit this dependency! -->
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
@@ -376,6 +350,70 @@
<version>3.10.0</version> <version>3.10.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- utils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!-- http -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<!-- logging -->
<dependency>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- xml -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -418,10 +456,11 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version> <version>2.22.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version> <version>3.0.0-M1</version>
<executions> <executions>
<execution> <execution>
<id>enforce-java</id> <id>enforce-java</id>
@@ -443,11 +482,31 @@
<requireMavenVersion> <requireMavenVersion>
<version>[3.1,)</version> <version>[3.1,)</version>
</requireMavenVersion> </requireMavenVersion>
<!--
enforce java 1.8 compatible bytecode
-->
<enforceBytecodeVersion>
<maxJdkVersion>1.8</maxJdkVersion>
<ignoreClasses>
<!--
ignore java 9 module info classes
because jaxb is compiled with java 7 expect of module-info, which is compiled with java 9
-->
<ignoreClass>module-info</ignoreClass>
</ignoreClasses>
</enforceBytecodeVersion>
</rules> </rules>
<fail>true</fail> <fail>true</fail>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
<dependencies>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>extra-enforcer-rules</artifactId>
<version>1.0-beta-7</version>
</dependency>
</dependencies>
</plugin> </plugin>
<plugin> <plugin>
@@ -636,9 +695,15 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId> <artifactId>maven-site-plugin</artifactId>
<version>3.2</version> <version>3.7</version>
<configuration> </plugin>
<reportPlugins>
</plugins>
</build>
<reporting>
<plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@@ -669,19 +734,13 @@
<artifactId>maven-pmd-plugin</artifactId> <artifactId>maven-pmd-plugin</artifactId>
<version>2.7.1</version> <version>2.7.1</version>
<configuration> <configuration>
<linkXref>true</linkXref>
<sourceEncoding>${project.build.sourceEncoding}</sourceEncoding> <sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
<targetJdk>${project.build.javaLevel}</targetJdk> <targetJdk>${project.build.javaLevel}</targetJdk>
</configuration> </configuration>
</plugin> </plugin>
</reportPlugins>
</configuration>
</plugin>
</plugins> </plugins>
</reporting>
</build>
<profiles> <profiles>
@@ -726,17 +785,9 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.0</version>
<configuration> <configuration>
<doclet>org.jboss.apiviz.APIviz</doclet> <failOnError>false</failOnError>
<docletArtifact>
<groupId>org.jboss.apiviz</groupId>
<artifactId>apiviz</artifactId>
<version>1.3.2.GA</version>
</docletArtifact>
<additionalparam>
-sourceclasspath ${project.build.outputDirectory}
-nopackagediagram
</additionalparam>
</configuration> </configuration>
</plugin> </plugin>
@@ -770,8 +821,8 @@
<junit.version>5.2.0</junit.version> <junit.version>5.2.0</junit.version>
<!-- logging libraries --> <!-- logging libraries -->
<slf4j.version>1.7.22</slf4j.version> <slf4j.version>1.7.25</slf4j.version>
<logback.version>1.1.10</logback.version> <logback.version>1.2.3</logback.version>
<servlet.version>3.0.1</servlet.version> <servlet.version>3.0.1</servlet.version>
<jaxrs.version>2.0.1</jaxrs.version> <jaxrs.version>2.0.1</jaxrs.version>
@@ -780,21 +831,22 @@
<enunciate.version>2.11.1</enunciate.version> <enunciate.version>2.11.1</enunciate.version>
<jackson.version>2.8.6</jackson.version> <jackson.version>2.8.6</jackson.version>
<guice.version>4.0</guice.version> <guice.version>4.0</guice.version>
<jaxb.version>2.3.0</jaxb.version>
<!-- event bus --> <!-- event bus -->
<legman.version>1.4.2</legman.version> <legman.version>1.4.2</legman.version>
<!-- webserver --> <!-- webserver -->
<jetty.version>9.2.10.v20150310</jetty.version> <jetty.version>9.4.14.v20181114</jetty.version>
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version> <jetty.maven.version>9.4.14.v20181114</jetty.maven.version>
<!-- security libraries --> <!-- security libraries -->
<ssp.version>1.1.0</ssp.version> <ssp.version>1.1.0</ssp.version>
<shiro.version>1.4.0</shiro.version> <shiro.version>1.4.0</shiro.version>
<!-- repostitory libraries --> <!-- repository libraries -->
<jgit.version>v4.5.2.201704071617-r-scm1</jgit.version> <jgit.version>v4.5.3.201708160445-r-scm1</jgit.version>
<svnkit.version>1.8.15-scm1</svnkit.version> <svnkit.version>1.9.0-scm3</svnkit.version>
<!-- util libraries --> <!-- util libraries -->
<guava.version>26.0-jre</guava.version> <guava.version>26.0-jre</guava.version>
@@ -806,6 +858,7 @@
<!-- build properties --> <!-- build properties -->
<project.build.javaLevel>1.8</project.build.javaLevel> <project.build.javaLevel>1.8</project.build.javaLevel>
<project.test.javaLevel>1.8</project.test.javaLevel>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<netbeans.hint.license>SCM-BSD</netbeans.hint.license> <netbeans.hint.license>SCM-BSD</netbeans.hint.license>
<jdk.classifier /> <jdk.classifier />

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.base.Charsets;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
/**
* Implementation of {@link CipherStreamHandler} which uses AES. This version is used since version 1.60 for the
* cli client encryption.
*
* @author Sebastian Sdorra
* @since 1.60
*/
public class AesCipherStreamHandler implements CipherStreamHandler {
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5PADDING";
private static final String SECRET_KEY_ALGORITHM = "AES";
private static final int IV_LENGTH = 16;
private final SecureRandom random = new SecureRandom();
private final byte[] secretKey;
AesCipherStreamHandler(String secretKey) {
this.secretKey = secretKey.getBytes(Charsets.UTF_8);
}
@Override
public OutputStream encrypt(OutputStream outputStream) throws IOException {
Cipher cipher = createCipherForEncryption();
outputStream.write(cipher.getIV());
return new CipherOutputStream(outputStream, cipher);
}
@Override
public InputStream decrypt(InputStream inputStream) throws IOException {
Cipher cipher = createCipherForDecryption(inputStream);
return new CipherInputStream(inputStream, cipher);
}
private Cipher createCipherForDecryption(InputStream inputStream) throws IOException {
byte[] iv = createEmptyIvArray();
inputStream.read(iv);
return createCipher(Cipher.DECRYPT_MODE, iv);
}
private byte[] createEmptyIvArray() {
return new byte[IV_LENGTH];
}
private Cipher createCipherForEncryption() {
byte[] iv = generateIV();
return createCipher(Cipher.ENCRYPT_MODE, iv);
}
private byte[] generateIV() {
// use 12 byte as described at nist
// https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
byte[] iv = createEmptyIvArray();
random.nextBytes(iv);
return iv;
}
private Cipher createCipher(int mode, byte[] iv) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(mode, new SecretKeySpec(secretKey, SECRET_KEY_ALGORITHM), ivParameterSpec);
return cipher;
} catch (Exception ex) {
throw new ScmConfigException("failed to create cipher", ex);
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* The CipherStreamHandler is able to encrypt and decrypt streams.
*
* @author Sebastian Sdorra
* @since 1.60
*/
public interface CipherStreamHandler {
/**
* Decrypts the given input stream.
*
* @param inputStream encrypted input stream
*
* @return raw input stream
*/
InputStream decrypt(InputStream inputStream) throws IOException;
/**
* Encrypts the given output stream.
*
* @param outputStream raw output stream
*
* @return encrypting output stream
*/
OutputStream encrypt(OutputStream outputStream) throws IOException;
}

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import sonia.scm.security.KeyGenerator;
import javax.xml.bind.JAXB;
import java.io.*;
import java.util.Arrays;
/**
* Util methods for configuration files.
*
* @author Sebastian Sdorra
* @since 1.60
*/
final class ConfigFiles {
private static final KeyGenerator keyGenerator = new SecureRandomKeyGenerator();
// SCM Config Version 2
@VisibleForTesting
static final byte[] VERSION_IDENTIFIER = "SCV2".getBytes(Charsets.US_ASCII);
private ConfigFiles() {
}
/**
* Returns {@code true} if the file is encrypted with the v2 format.
*
* @param file configuration file
*
* @return {@code true} for format v2
*
* @throws IOException
*/
static boolean isFormatV2(File file) throws IOException {
try (InputStream input = new FileInputStream(file)) {
byte[] bytes = new byte[VERSION_IDENTIFIER.length];
input.read(bytes);
return Arrays.equals(VERSION_IDENTIFIER, bytes);
}
}
/**
* Decrypt and parse v1 configuration file.
*
* @param secretKeyStore key store
* @param file configuration file
*
* @return client configuration
*
* @throws IOException
*/
static ScmClientConfig parseV1(SecretKeyStore secretKeyStore, File file) throws IOException {
String secretKey = secretKey(secretKeyStore);
CipherStreamHandler cipherStreamHandler = new WeakCipherStreamHandler(secretKey);
return decrypt(cipherStreamHandler, new FileInputStream(file));
}
/**
* Decrypt and parse v12configuration file.
*
* @param secretKeyStore key store
* @param file configuration file
*
* @return client configuration
*
* @throws IOException
*/
static ScmClientConfig parseV2(SecretKeyStore secretKeyStore, File file) throws IOException {
String secretKey = secretKey(secretKeyStore);
CipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(secretKey);
try (InputStream input = new FileInputStream(file)) {
input.skip(VERSION_IDENTIFIER.length);
return decrypt(cipherStreamHandler, input);
}
}
/**
* Store encrypt and write the configuration to the given file.
* Note the method uses always the latest available format.
*
* @param secretKeyStore key store
* @param config configuration
* @param file configuration file
*
* @throws IOException
*/
static void store(SecretKeyStore secretKeyStore, ScmClientConfig config, File file) throws IOException {
String secretKey = keyGenerator.createKey();
CipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(secretKey);
try (OutputStream output = new FileOutputStream(file)) {
output.write(VERSION_IDENTIFIER);
encrypt(cipherStreamHandler, output, config);
}
secretKeyStore.set(secretKey);
}
private static String secretKey(SecretKeyStore secretKeyStore) {
String secretKey = secretKeyStore.get();
Preconditions.checkState(!Strings.isNullOrEmpty(secretKey), "no stored secret key found");
return secretKey;
}
private static ScmClientConfig decrypt(CipherStreamHandler cipherStreamHandler, InputStream input) throws IOException {
try ( InputStream decryptedInputStream = cipherStreamHandler.decrypt(input) ) {
return JAXB.unmarshal(decryptedInputStream, ScmClientConfig.class);
}
}
private static void encrypt(CipherStreamHandler cipherStreamHandler, OutputStream output, ScmClientConfig clientConfig) throws IOException {
try ( OutputStream encryptedOutputStream = cipherStreamHandler.encrypt(output) ) {
JAXB.marshal(clientConfig, encryptedOutputStream);
}
}
}

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* The EncryptionSecretKeyStoreWrapper is a wrapper around the {@link SecretKeyStore} interface. The wrapper will
* encrypt the passed secret keys, before they are written to the underlying {@link SecretKeyStore} implementation. The
* wrapper will also honor old unencrypted keys.
*
* @author Sebastian Sdorra
* @since 1.60
*/
public class EncryptionSecretKeyStoreWrapper implements SecretKeyStore {
private static final String ALGORITHM = "AES";
private static final SecureRandom random = new SecureRandom();
// i know storing the key directly in the class is far away from a best practice, but this is a chicken egg type
// of problem. We need a key to encrypt the stored keys, however encrypting the keys with a static defined key
// is better as storing them as plain text.
private static final byte[] SECRET_KEY = new byte[]{ 0x50, 0x61, 0x41, 0x67, 0x55, 0x43, 0x48, 0x7a, 0x48, 0x59,
0x7a, 0x57, 0x6b, 0x34, 0x54, 0x62
};
@VisibleForTesting
static final String ENCRYPTED_PREFIX = "SKV2:";
private SecretKeyStore wrappedSecretKeyStore;
EncryptionSecretKeyStoreWrapper(SecretKeyStore wrappedSecretKeyStore) {
this.wrappedSecretKeyStore = wrappedSecretKeyStore;
}
@Override
public void set(String secretKey) {
String encrypted = encrypt(secretKey);
wrappedSecretKeyStore.set(ENCRYPTED_PREFIX.concat(encrypted));
}
private String encrypt(String value) {
try {
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE);
byte[] raw = cipher.doFinal(value.getBytes(Charsets.UTF_8));
return encode(raw);
} catch (IllegalBlockSizeException | BadPaddingException ex) {
throw new ScmConfigException("failed to encrypt key", ex);
}
}
private String encode(byte[] raw) {
return BaseEncoding.base64().encode(raw);
}
@Override
public String get() {
String value = wrappedSecretKeyStore.get();
if (Strings.nullToEmpty(value).startsWith(ENCRYPTED_PREFIX)) {
String encrypted = value.substring(ENCRYPTED_PREFIX.length());
return decrypt(encrypted);
}
return value;
}
private String decrypt(String encoded) {
try {
Cipher cipher = createCipher(Cipher.DECRYPT_MODE);
byte[] raw = decode(encoded);
return new String(cipher.doFinal(raw), Charsets.UTF_8);
} catch (IllegalBlockSizeException | BadPaddingException ex) {
throw new ScmConfigException("failed to decrypt key", ex);
}
}
private byte[] decode(String encoded) {
return BaseEncoding.base64().decode(encoded);
}
private Cipher createCipher(int mode) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY, "AES");
cipher.init(mode, secretKeySpec, random);
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException ex) {
throw new ScmConfigException("failed to create key", ex);
}
}
@Override
public void remove() {
wrappedSecretKeyStore.remove();
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import java.util.prefs.Preferences;
/**
* SecretKeyStore implementation with uses {@link Preferences}.
*
* @author Sebastian Sdorra
* @since 1.60
*/
public class PrefsSecretKeyStore implements SecretKeyStore {
private static final String PREF_SECRET_KEY = "scm.client.key";
private final Preferences preferences;
PrefsSecretKeyStore() {
// we use ScmClientConfigFileHandler as base for backward compatibility
preferences = Preferences.userNodeForPackage(ScmClientConfigFileHandler.class);
}
@Override
public void set(String secretKey) {
preferences.put(PREF_SECRET_KEY, secretKey);
}
@Override
public String get() {
return preferences.get(PREF_SECRET_KEY, null);
}
@Override
public void remove() {
preferences.remove(PREF_SECRET_KEY);
}
}

View File

@@ -29,71 +29,32 @@
* *
*/ */
package sonia.scm.cli.config;
package sonia.scm.repository;
/** /**
* Type of permissionPrefix for a {@link Repository}. * SecretKeyStore is able to read and write secret keys.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 1.60
*/ */
public enum PermissionType public interface SecretKeyStore {
{
/** read permision */
READ(0, "repository:read,pull:"),
/** read and write permissionPrefix */
WRITE(10, "repository:read,pull,push:"),
/** /**
* read, write and * Writes the given secret key to the store.
* also the ability to manage the properties and permissions *
* @param secretKey secret key to write
*/ */
OWNER(100, "repository:*:"); void set(String secretKey);
/** /**
* Constructs a new permissionPrefix type * Reads the secret key from the store. The method returns {@code null} if no secret key was stored.
* *
* * @return secret key or {@code null}
* @param value
*/ */
private PermissionType(int value, String permissionPrefix) String get();
{
this.value = value;
this.permissionPrefix = permissionPrefix;
}
//~--- get methods ----------------------------------------------------------
/** /**
* * Removes the secret key from store.
* @return
*
* @since 2.0.0
*/ */
public String getPermissionPrefix() void remove();
{
return permissionPrefix;
}
/**
* Returns the integer representation of the {@link PermissionType}
*
*
* @return integer representation
*/
public int getValue()
{
return value;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final String permissionPrefix;
/** Field description */
private final int value;
} }

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.annotations.VisibleForTesting;
import sonia.scm.security.KeyGenerator;
import java.security.SecureRandom;
import java.util.Locale;
/**
* Create keys by using {@link SecureRandom}. The SecureRandomKeyGenerator produces aes compatible keys.
* Warning the class is not thread safe.
*
* @author Sebastian Sdorra
* @since 1.60
*/
public class SecureRandomKeyGenerator implements KeyGenerator {
private SecureRandom random = new SecureRandom();
// key length 16 for aes128
@VisibleForTesting
static final int KEY_LENGTH = 16;
private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String LOWER = UPPER.toLowerCase(Locale.ENGLISH);
private static final String DIGITS = "0123456789";
private static final char[] ALL = (UPPER + LOWER + DIGITS).toCharArray();
@Override
public String createKey() {
char[] key = new char[KEY_LENGTH];
for (int idx = 0; idx < KEY_LENGTH; ++idx) {
key[idx] = ALL[random.nextInt(ALL.length)];
}
return new String(key);
}
}

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
/**
* Weak implementation of {@link CipherStreamHandler}. This is the old implementation, which was used in versions prior
* 1.60.
*
* @author Sebastian Sdorra
* @since 1.60
*
* @see <a href="https://bitbucket.org/sdorra/scm-manager/issues/978/iteration-count-for-password-based">Issue 978</a>
* @see <a href="https://bitbucket.org/sdorra/scm-manager/issues/979/constant-salts-for-pbe-are-insecure">Issue 979</a>
*/
public class WeakCipherStreamHandler implements CipherStreamHandler {
private static final String SALT = "AE16347F";
private static final int SPEC_ITERATION = 12;
private static final String CIPHER_NAME = "PBEWithMD5AndDES";
private final char[] secretKey;
/**
* Creates a new handler with the given secret key.
*
* @param secretKey secret key
*/
WeakCipherStreamHandler(String secretKey) {
this.secretKey = secretKey.toCharArray();
}
@Override
public InputStream decrypt(InputStream inputStream) {
try {
Cipher c = createCipher(Cipher.DECRYPT_MODE);
return new CipherInputStream(inputStream, c);
} catch (Exception ex) {
throw new ScmConfigException("could not encrypt output stream", ex);
}
}
@Override
public OutputStream encrypt(OutputStream outputStream) {
try {
Cipher c = createCipher(Cipher.ENCRYPT_MODE);
return new CipherOutputStream(outputStream, c);
} catch (Exception ex) {
throw new ScmConfigException("could not encrypt output stream", ex);
}
}
private Cipher createCipher(int mode)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeySpecException, InvalidKeyException,
InvalidAlgorithmParameterException
{
SecretKey sk = createSecretKey();
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
PBEParameterSpec spec = new PBEParameterSpec(SALT.getBytes(), SPEC_ITERATION);
cipher.init(mode, sk, spec);
return cipher;
}
private SecretKey createSecretKey()
throws NoSuchAlgorithmException, InvalidKeySpecException
{
PBEKeySpec keySpec = new PBEKeySpec(secretKey);
SecretKeyFactory factory = SecretKeyFactory.getInstance(CIPHER_NAME);
return factory.generateSecret(keySpec);
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import org.junit.Test;
import sonia.scm.security.KeyGenerator;
import java.io.*;
import static org.junit.Assert.assertEquals;
public class AesCipherStreamHandlerTest {
private final KeyGenerator keyGenerator = new SecureRandomKeyGenerator();
@Test
public void testEncryptAndDecrypt() throws IOException {
AesCipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(keyGenerator.createKey());
// douglas adams
String content = "If you try and take a cat apart to see how it works, the first thing you have on your hands is a nonworking cat.";
// encrypt
ByteArrayOutputStream output = new ByteArrayOutputStream();
OutputStream encryptedOutput = cipherStreamHandler.encrypt(output);
encryptedOutput.write(content.getBytes(Charsets.UTF_8));
encryptedOutput.close();
InputStream input = new ByteArrayInputStream(output.toByteArray());
input = cipherStreamHandler.decrypt(input);
byte[] decrypted = ByteStreams.toByteArray(input);
assertEquals(content, new String(decrypted, Charsets.UTF_8));
}
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import javax.xml.bind.JAXB;
import java.io.*;
import static org.junit.Assert.assertEquals;
final class ClientConfigurationTests {
private ClientConfigurationTests() {
}
static void testCipherStream(CipherStreamHandler cipherStreamHandler, String content) throws IOException {
byte[] encrypted = encrypt(cipherStreamHandler, content);
String decrypted = decrypt(cipherStreamHandler, encrypted);
assertEquals(content, decrypted);
}
static byte[] encrypt(CipherStreamHandler cipherStreamHandler, String content) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
OutputStream encryptedOutput = cipherStreamHandler.encrypt(output);
encryptedOutput.write(content.getBytes(Charsets.UTF_8));
encryptedOutput.close();
return output.toByteArray();
}
static String decrypt(CipherStreamHandler cipherStreamHandler, byte[] encrypted) throws IOException {
InputStream input = new ByteArrayInputStream(encrypted);
input = cipherStreamHandler.decrypt(input);
byte[] decrypted = ByteStreams.toByteArray(input);
input.close();
return new String(decrypted, Charsets.UTF_8);
}
static void assertSampleConfig(ScmClientConfig config) {
ServerConfig defaultConfig;
defaultConfig = config.getDefaultConfig();
assertEquals("http://localhost:8080/scm", defaultConfig.getServerUrl());
assertEquals("admin", defaultConfig.getUsername());
assertEquals("admin123", defaultConfig.getPassword());
}
static ScmClientConfig createSampleConfig() {
ScmClientConfig config = new ScmClientConfig();
ServerConfig defaultConfig = config.getDefaultConfig();
defaultConfig.setServerUrl("http://localhost:8080/scm");
defaultConfig.setUsername("admin");
defaultConfig.setPassword("admin123");
return config;
}
static void encrypt(CipherStreamHandler cipherStreamHandler, ScmClientConfig config, File file) throws IOException {
try (OutputStream output = cipherStreamHandler.encrypt(new FileOutputStream(file))) {
JAXB.marshal(config, output);
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.*;
public class ConfigFilesTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void testIsFormatV2() throws IOException {
byte[] content = "The door was the way to... to... The Door was The Way".getBytes(Charsets.UTF_8);
File fileV1 = temporaryFolder.newFile();
Files.write(content, fileV1);
assertFalse(ConfigFiles.isFormatV2(fileV1));
File fileV2 = temporaryFolder.newFile();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(ConfigFiles.VERSION_IDENTIFIER);
baos.write(content);
Files.write(baos.toByteArray(), fileV2);
assertTrue(ConfigFiles.isFormatV2(fileV2));
}
@Test
public void testParseV1() throws IOException {
InMemorySecretKeyStore keyStore = createKeyStore();
WeakCipherStreamHandler handler = new WeakCipherStreamHandler(keyStore.get());
ScmClientConfig config = ClientConfigurationTests.createSampleConfig();
File file = temporaryFolder.newFile();
ClientConfigurationTests.encrypt(handler, config, file);
config = ConfigFiles.parseV1(keyStore, file);
ClientConfigurationTests.assertSampleConfig(config);
}
@Test
public void storeAndParseV2() throws IOException {
InMemorySecretKeyStore keyStore = new InMemorySecretKeyStore();
ScmClientConfig config = ClientConfigurationTests.createSampleConfig();
File file = temporaryFolder.newFile();
ConfigFiles.store(keyStore, config, file);
String key = keyStore.get();
assertNotNull(key);
config = ConfigFiles.parseV2(keyStore, file);
ClientConfigurationTests.assertSampleConfig(config);
}
private InMemorySecretKeyStore createKeyStore() {
String secretKey = new SecureRandomKeyGenerator().createKey();
InMemorySecretKeyStore keyStore = new InMemorySecretKeyStore();
keyStore.set(secretKey);
return keyStore;
}
}

View File

@@ -30,58 +30,31 @@
*/ */
package sonia.scm.security;
//~--- non-JDK imports -------------------------------------------------------- package sonia.scm.cli.config;
import org.junit.Test; import org.junit.Test;
import sonia.scm.repository.PermissionType;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/** public class EncryptionSecretKeyStoreWrapperTest {
*
* @author Sebastian Sdorra private SecretKeyStore secretKeyStore = new InMemorySecretKeyStore();
*/
public class RepositoryPermissionTest
{
/**
* Method description
*
*/
@Test @Test
public void testImplies() public void testEncryptionKeyStoreWrapper() {
{ EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore);
RepositoryPermission p = new RepositoryPermission("asd", wrapper.set("mysecretkey");
PermissionType.READ);
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ))); assertEquals("mysecretkey", wrapper.get());
assertFalse(p.implies(new RepositoryPermission("asd", assertTrue(secretKeyStore.get().startsWith(EncryptionSecretKeyStoreWrapper.ENCRYPTED_PREFIX));
PermissionType.OWNER)));
assertFalse(p.implies(new RepositoryPermission("asd",
PermissionType.WRITE)));
p = new RepositoryPermission("asd", PermissionType.OWNER);
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
assertFalse(p.implies(new RepositoryPermission("bdb",
PermissionType.READ)));
} }
/**
* Method description
*
*/
@Test @Test
public void testImpliesWithWildcard() public void testEncryptionKeyStoreWrapperWithOldUnencryptedKey() {
{ secretKeyStore.set("mysecretkey");
RepositoryPermission p = new RepositoryPermission("*", EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore);
PermissionType.OWNER); assertEquals("mysecretkey", wrapper.get());
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
assertTrue(p.implies(new RepositoryPermission("bdb",
PermissionType.OWNER)));
assertTrue(p.implies(new RepositoryPermission("cgd",
PermissionType.WRITE)));
} }
} }

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
public class InMemorySecretKeyStore implements SecretKeyStore {
private String secretKey;
@Override
public void set(String secretKey) {
this.secretKey = secretKey;
}
@Override
public String get() {
return secretKey;
}
@Override
public void remove() {
this.secretKey = null;
}
}

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.security.UUIDKeyGenerator;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import static org.junit.Assert.*;
public class ScmClientConfigFileHandlerTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void testClientConfigFileHandler() throws IOException {
File configFile = temporaryFolder.newFile();
ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler(
new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()), configFile
);
ScmClientConfig config = new ScmClientConfig();
ServerConfig defaultConfig = config.getDefaultConfig();
defaultConfig.setServerUrl("http://localhost:8080/scm");
defaultConfig.setUsername("scmadmin");
defaultConfig.setPassword("admin123");
handler.write(config);
assertTrue(configFile.exists());
config = handler.read();
defaultConfig = config.getDefaultConfig();
assertEquals("http://localhost:8080/scm", defaultConfig.getServerUrl());
assertEquals("scmadmin", defaultConfig.getUsername());
assertEquals("admin123", defaultConfig.getPassword());
handler.delete();
assertFalse(configFile.exists());
}
@Test
public void testClientConfigFileHandlerWithOldConfiguration() throws IOException {
File configFile = temporaryFolder.newFile();
// old implementation has used uuids as keys
String key = new UUIDKeyGenerator().createKey();
WeakCipherStreamHandler weakCipherStreamHandler = new WeakCipherStreamHandler(key);
ScmClientConfig clientConfig = ClientConfigurationTests.createSampleConfig();
ClientConfigurationTests.encrypt(weakCipherStreamHandler, clientConfig, configFile);
assertFalse(ConfigFiles.isFormatV2(configFile));
SecretKeyStore secretKeyStore = new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore());
secretKeyStore.set(key);
ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler(
secretKeyStore, configFile
);
ScmClientConfig config = handler.read();
ClientConfigurationTests.assertSampleConfig(config);
// ensure key has changed
assertNotEquals(key, secretKeyStore.get());
// ensure config rewritten with v2
assertTrue(ConfigFiles.isFormatV2(configFile));
}
@Test
public void testClientConfigFileHandlerWithRealMigration() throws IOException {
URL resource = Resources.getResource("sonia/scm/cli/config/scm-cli-config.enc.xml");
byte[] bytes = Resources.toByteArray(resource);
File configFile = temporaryFolder.newFile();
Files.write(bytes, configFile);
String key = "358e018a-0c3c-4339-8266-3874e597305f";
SecretKeyStore secretKeyStore = new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore());
secretKeyStore.set(key);
ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler(
secretKeyStore, configFile
);
ScmClientConfig config = handler.read();
ServerConfig defaultConfig = config.getDefaultConfig();
assertEquals("http://hitchhicker.com/scm", defaultConfig.getServerUrl());
assertEquals("tricia", defaultConfig.getUsername());
assertEquals("trillian123", defaultConfig.getPassword());
// ensure key has changed
assertNotEquals(key, secretKeyStore.get());
// ensure config rewritten with v2
assertTrue(ConfigFiles.isFormatV2(configFile));
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.cli.config;
import org.junit.Test;
import static org.junit.Assert.*;
public class SecureRandomKeyGeneratorTest {
@Test
public void testCreateKey() {
SecureRandomKeyGenerator keyGenerator = new SecureRandomKeyGenerator();
assertNotNull(keyGenerator.createKey());
assertEquals(SecureRandomKeyGenerator.KEY_LENGTH, keyGenerator.createKey().length());
}
}

View File

@@ -47,6 +47,7 @@
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<version>${slf4j.version}</version> <version>${slf4j.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<artifactId>jcl-over-slf4j</artifactId> <artifactId>jcl-over-slf4j</artifactId>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
@@ -143,6 +144,28 @@
<version>${legman.version}</version> <version>${legman.version}</version>
</dependency> </dependency>
<!-- xml -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
<!-- util --> <!-- util -->
<dependency> <dependency>
@@ -193,6 +216,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.0</version>
<configuration> <configuration>
<useStandardDocletOptions>true</useStandardDocletOptions> <useStandardDocletOptions>true</useStandardDocletOptions>
<charset>${project.build.sourceEncoding}</charset> <charset>${project.build.sourceEncoding}</charset>
@@ -216,6 +240,20 @@
<link>http://www.slf4j.org/api/</link> <link>http://www.slf4j.org/api/</link>
<link>http://shiro.apache.org/static/${shiro.version}/apidocs/</link> <link>http://shiro.apache.org/static/${shiro.version}/apidocs/</link>
</links> </links>
<doclet>org.jboss.apiviz.APIviz</doclet>
<docletArtifact>
<groupId>org.jboss.apiviz</groupId>
<artifactId>apiviz</artifactId>
<version>1.3.2.GA</version>
</docletArtifact>
<additionalOptions>
<additionalOption>
-sourceclasspath ${project.build.outputDirectory}
</additionalOption>
<additionalOption>
-nopackagediagram
</additionalOption>
</additionalOptions>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
public abstract class BaseMapper<T, D extends HalRepresentation> extends LinkAppenderMapper implements InstantAttributeMapper { public abstract class BaseMapper<T, D extends HalRepresentation> extends HalAppenderMapper implements InstantAttributeMapper {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract D map(T modelObject); public abstract D map(T modelObject);

View File

@@ -1,5 +1,6 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import lombok.Getter; import lombok.Getter;
@@ -7,7 +8,6 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.time.Instant; import java.time.Instant;
import java.util.List;
@Getter @Getter
@Setter @Setter
@@ -34,16 +34,7 @@ public class ChangesetDto extends HalRepresentation {
*/ */
private String description; private String description;
@Override public ChangesetDto(Links links, Embedded embedded) {
@SuppressWarnings("squid:S1185") // We want to have this method available in this package super(links, embedded);
protected HalRepresentation add(Links links) {
return super.add(links);
} }
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation withEmbedded(String rel, List<? extends HalRepresentation> halRepresentations) {
return super.withEmbedded(rel, halRepresentations);
}
} }

View File

@@ -1,12 +1,14 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
/** /**
* The {@link LinkAppender} can be used within an {@link LinkEnricher} to append hateoas links to a json response. * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
public interface LinkAppender { public interface HalAppender {
/** /**
* Appends one link to the json response. * Appends one link to the json response.
@@ -14,7 +16,7 @@ public interface LinkAppender {
* @param rel name of relation * @param rel name of relation
* @param href link uri * @param href link uri
*/ */
void appendOne(String rel, String href); void appendLink(String rel, String href);
/** /**
* Returns a builder which is able to append an array of links to the resource. * Returns a builder which is able to append an array of links to the resource.
@@ -22,8 +24,15 @@ public interface LinkAppender {
* @param rel name of link relation * @param rel name of link relation
* @return multi link builder * @return multi link builder
*/ */
LinkArrayBuilder arrayBuilder(String rel); LinkArrayBuilder linkArrayBuilder(String rel);
/**
* Appends one embedded to the json response.
*
* @param rel name of relation
* @param embeddedItem embedded object
*/
void appendEmbedded(String rel, HalRepresentation embeddedItem);
/** /**
* Builder for link arrays. * Builder for link arrays.

View File

@@ -4,17 +4,17 @@ import com.google.common.annotations.VisibleForTesting;
import javax.inject.Inject; import javax.inject.Inject;
public class LinkAppenderMapper { public class HalAppenderMapper {
@Inject @Inject
private LinkEnricherRegistry registry; private HalEnricherRegistry registry;
@VisibleForTesting @VisibleForTesting
void setRegistry(LinkEnricherRegistry registry) { void setRegistry(HalEnricherRegistry registry) {
this.registry = registry; this.registry = registry;
} }
protected void appendLinks(LinkAppender appender, Object source, Object... contextEntries) { protected void applyEnrichers(HalAppender appender, Object source, Object... contextEntries) {
// null check is only their to not break existing tests // null check is only their to not break existing tests
if (registry != null) { if (registry != null) {
@@ -24,10 +24,10 @@ public class LinkAppenderMapper {
ctx[i + 1] = contextEntries[i]; ctx[i + 1] = contextEntries[i];
} }
LinkEnricherContext context = LinkEnricherContext.of(ctx); HalEnricherContext context = HalEnricherContext.of(ctx);
Iterable<LinkEnricher> enrichers = registry.allByType(source.getClass()); Iterable<HalEnricher> enrichers = registry.allByType(source.getClass());
for (LinkEnricher enricher : enrichers) { for (HalEnricher enricher : enrichers) {
enricher.enrich(context, appender); enricher.enrich(context, appender);
} }
} }

View File

@@ -3,8 +3,8 @@ package sonia.scm.api.v2.resources;
import sonia.scm.plugin.ExtensionPoint; import sonia.scm.plugin.ExtensionPoint;
/** /**
* A {@link LinkEnricher} can be used to append hateoas links to a specific json response. * A {@link HalEnricher} can be used to append hal specific attributes, such as links, to the json response.
* To register an enricher use the {@link Enrich} annotation or the {@link LinkEnricherRegistry} which is available * To register an enricher use the {@link Enrich} annotation or the {@link HalEnricherRegistry} which is available
* via injection. * via injection.
* *
* <b>Warning:</b> enrichers are always registered as singletons. * <b>Warning:</b> enrichers are always registered as singletons.
@@ -14,13 +14,13 @@ import sonia.scm.plugin.ExtensionPoint;
*/ */
@ExtensionPoint @ExtensionPoint
@FunctionalInterface @FunctionalInterface
public interface LinkEnricher { public interface HalEnricher {
/** /**
* Enriches the response with hateoas links. * Enriches the response with hal specific attributes.
* *
* @param context contains the source for the json mapping and related objects * @param context contains the source for the json mapping and related objects
* @param appender can be used to append links to the json response * @param appender can be used to append links or embedded objects to the json response
*/ */
void enrich(LinkEnricherContext context, LinkAppender appender); void enrich(HalEnricherContext context, HalAppender appender);
} }

View File

@@ -7,17 +7,17 @@ import java.util.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
/** /**
* Context object for the {@link LinkEnricher}. The context holds the source object for the json and all related * Context object for the {@link HalEnricher}. The context holds the source object for the json and all related
* objects, which can be useful for the link creation. * objects, which can be useful for the enrichment.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
public final class LinkEnricherContext { public final class HalEnricherContext {
private final Map<Class, Object> instanceMap; private final Map<Class, Object> instanceMap;
private LinkEnricherContext(Map<Class,Object> instanceMap) { private HalEnricherContext(Map<Class,Object> instanceMap) {
this.instanceMap = instanceMap; this.instanceMap = instanceMap;
} }
@@ -28,12 +28,12 @@ public final class LinkEnricherContext {
* *
* @return context of given entries * @return context of given entries
*/ */
public static LinkEnricherContext of(Object... instances) { public static HalEnricherContext of(Object... instances) {
ImmutableMap.Builder<Class, Object> builder = ImmutableMap.builder(); ImmutableMap.Builder<Class, Object> builder = ImmutableMap.builder();
for (Object instance : instances) { for (Object instance : instances) {
builder.put(instance.getClass(), instance); builder.put(instance.getClass(), instance);
} }
return new LinkEnricherContext(builder.build()); return new HalEnricherContext(builder.build());
} }
/** /**

View File

@@ -7,34 +7,34 @@ import sonia.scm.plugin.Extension;
import javax.inject.Singleton; import javax.inject.Singleton;
/** /**
* The {@link LinkEnricherRegistry} is responsible for binding {@link LinkEnricher} instances to their source types. * The {@link HalEnricherRegistry} is responsible for binding {@link HalEnricher} instances to their source types.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
@Extension @Extension
@Singleton @Singleton
public final class LinkEnricherRegistry { public final class HalEnricherRegistry {
private final Multimap<Class, LinkEnricher> enrichers = HashMultimap.create(); private final Multimap<Class, HalEnricher> enrichers = HashMultimap.create();
/** /**
* Registers a new {@link LinkEnricher} for the given source type. * Registers a new {@link HalEnricher} for the given source type.
* *
* @param sourceType type of json mapping source * @param sourceType type of json mapping source
* @param enricher link enricher instance * @param enricher link enricher instance
*/ */
public void register(Class sourceType, LinkEnricher enricher) { public void register(Class sourceType, HalEnricher enricher) {
enrichers.put(sourceType, enricher); enrichers.put(sourceType, enricher);
} }
/** /**
* Returns all registered {@link LinkEnricher} for the given type. * Returns all registered {@link HalEnricher} for the given type.
* *
* @param sourceType type of json mapping source * @param sourceType type of json mapping source
* @return all registered enrichers * @return all registered enrichers
*/ */
public Iterable<LinkEnricher> allByType(Class sourceType) { public Iterable<HalEnricher> allByType(Class sourceType) {
return enrichers.get(sourceType); return enrichers.get(sourceType);
} }
} }

View File

@@ -1,7 +1,7 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
/** /**
* The {@link Index} object can be used to register a {@link LinkEnricher} for the index resource. * The {@link Index} object can be used to register a {@link HalEnricher} for the index resource.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0

View File

@@ -1,7 +1,7 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
/** /**
* The {@link Me} object can be used to register a {@link LinkEnricher} for the me resource. * The {@link Me} object can be used to register a {@link HalEnricher} for the me resource.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0

View File

@@ -68,7 +68,6 @@ import java.util.Set;
@XmlRootElement(name = "repositories") @XmlRootElement(name = "repositories")
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
private static final long serialVersionUID = 3486560714961909711L; private static final long serialVersionUID = 3486560714961909711L;
private String contact; private String contact;
@@ -81,6 +80,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified; private Long lastModified;
private String namespace; private String namespace;
private String name; private String name;
@XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>(); private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public") @XmlElement(name = "public")
private boolean publicReadable = false; private boolean publicReadable = false;

View File

@@ -1,6 +1,5 @@
package sonia.scm.repository; package sonia.scm.repository;
import groovy.lang.Singleton;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import javax.inject.Inject; import javax.inject.Inject;
@@ -18,7 +17,6 @@ import java.nio.file.Path;
* @author Mohamed Karray * @author Mohamed Karray
* @since 2.0.0 * @since 2.0.0
*/ */
@Singleton
public class RepositoryLocationResolver { public class RepositoryLocationResolver {
private final SCMContextProvider contextProvider; private final SCMContextProvider contextProvider;

View File

@@ -37,12 +37,19 @@ package sonia.scm.repository;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import org.apache.commons.collections.CollectionUtils;
import sonia.scm.security.PermissionObject; import sonia.scm.security.PermissionObject;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedHashSet;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -60,54 +67,19 @@ public class RepositoryPermission implements PermissionObject, Serializable
private boolean groupPermission = false; private boolean groupPermission = false;
private String name; private String name;
private PermissionType type = PermissionType.READ; @XmlElement(name = "verb")
private Collection<String> verbs;
/** /**
* Constructs a new {@link RepositoryPermission}. * Constructs a new {@link RepositoryPermission}.
* This constructor is used by JAXB. * This constructor is used by JAXB and mapstruct.
*
*/ */
public RepositoryPermission() {} public RepositoryPermission() {}
/** public RepositoryPermission(String name, Collection<String> verbs, boolean groupPermission)
* Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ}
* for the specified user.
*
*
* @param name name of the user
*/
public RepositoryPermission(String name)
{ {
this();
this.name = name; this.name = name;
} this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs));
/**
* Constructs a new {@link RepositoryPermission} with the specified type for
* the given user.
*
*
* @param name name of the user
* @param type type of the permission
*/
public RepositoryPermission(String name, PermissionType type)
{
this(name);
this.type = type;
}
/**
* Constructs a new {@link RepositoryPermission} with the specified type for
* the given user or group.
*
*
* @param name name of the user or group
* @param type type of the permission
* @param groupPermission true if the permission is a permission for a group
*/
public RepositoryPermission(String name, PermissionType type, boolean groupPermission)
{
this(name, type);
this.groupPermission = groupPermission; this.groupPermission = groupPermission;
} }
@@ -137,7 +109,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
final RepositoryPermission other = (RepositoryPermission) obj; final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name) return Objects.equal(name, other.name)
&& Objects.equal(type, other.type) && CollectionUtils.isEqualCollection(verbs, other.verbs)
&& Objects.equal(groupPermission, other.groupPermission); && Objects.equal(groupPermission, other.groupPermission);
} }
@@ -150,7 +122,9 @@ public class RepositoryPermission implements PermissionObject, Serializable
@Override @Override
public int hashCode() public int hashCode()
{ {
return Objects.hashCode(name, type, groupPermission); // Normally we do not have a log of repository permissions having the same size of verbs, but different content.
// Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
return Objects.hashCode(name, verbs.size(), groupPermission);
} }
@@ -160,7 +134,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
//J- //J-
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("name", name) .add("name", name)
.add("type", type) .add("verbs", verbs)
.add("groupPermission", groupPermission) .add("groupPermission", groupPermission)
.toString(); .toString();
//J+ //J+
@@ -181,14 +155,14 @@ public class RepositoryPermission implements PermissionObject, Serializable
} }
/** /**
* Returns the {@link PermissionType} of the permission. * Returns the verb of the permission.
* *
* *
* @return {@link PermissionType} of the permission * @return verb of the permission
*/ */
public PermissionType getType() public Collection<String> getVerbs()
{ {
return type; return verbs == null? emptyList(): verbs;
} }
/** /**
@@ -228,13 +202,13 @@ public class RepositoryPermission implements PermissionObject, Serializable
} }
/** /**
* Sets the type of the permission. * Sets the verb of the permission.
* *
* *
* @param type type of the permission * @param verbs verbs of the permission
*/ */
public void setType(PermissionType type) public void setVerbs(Collection<String> verbs)
{ {
this.type = type; this.verbs = verbs;
} }
} }

View File

@@ -39,12 +39,11 @@ import org.apache.shiro.subject.Subject;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.IncomingCommand; import sonia.scm.repository.spi.IncomingCommand;
import sonia.scm.repository.spi.IncomingCommandRequest; import sonia.scm.repository.spi.IncomingCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
@@ -94,8 +93,7 @@ public final class IncomingCommandBuilder
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
subject.checkPermission(new RepositoryPermission(remoteRepository, subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
PermissionType.READ));
request.setRemoteRepository(remoteRepository); request.setRemoteRepository(remoteRepository);

View File

@@ -34,12 +34,11 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.OutgoingCommand; import sonia.scm.repository.spi.OutgoingCommand;
import sonia.scm.repository.spi.OutgoingCommandRequest; import sonia.scm.repository.spi.OutgoingCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
@@ -84,8 +83,7 @@ public final class OutgoingCommandBuilder
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
subject.checkPermission(new RepositoryPermission(remoteRepository, subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
PermissionType.READ));
request.setRemoteRepository(remoteRepository); request.setRemoteRepository(remoteRepository);

View File

@@ -38,11 +38,10 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.PullCommand; import sonia.scm.repository.spi.PullCommand;
import sonia.scm.repository.spi.PullCommandRequest; import sonia.scm.repository.spi.PullCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@@ -96,9 +95,7 @@ public final class PullCommandBuilder
public PullResponse pull(String url) throws IOException { public PullResponse pull(String url) throws IOException {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.checkPermission( subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
new RepositoryPermission(localRepository, PermissionType.WRITE)
);
//J+ //J+
URL remoteUrl = new URL(url); URL remoteUrl = new URL(url);
@@ -124,12 +121,8 @@ public final class PullCommandBuilder
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.checkPermission( subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
new RepositoryPermission(localRepository, PermissionType.WRITE) subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
);
subject.checkPermission(
new RepositoryPermission(remoteRepository, PermissionType.READ)
);
//J+ //J+
request.reset(); request.reset();

View File

@@ -39,11 +39,10 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.PushCommand; import sonia.scm.repository.spi.PushCommand;
import sonia.scm.repository.spi.PushCommandRequest; import sonia.scm.repository.spi.PushCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@@ -92,9 +91,7 @@ public final class PushCommandBuilder
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.checkPermission( subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
new RepositoryPermission(remoteRepository, PermissionType.WRITE)
);
//J+ //J+
logger.info("push changes to repository {}", remoteRepository.getId()); logger.info("push changes to repository {}", remoteRepository.getId());

View File

@@ -1,230 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import org.apache.shiro.authz.Permission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import java.io.Serializable;
//~--- JDK imports ------------------------------------------------------------
/**
* This class represents the permission to a repository of a user.
*
* @author Sebastian Sdorra
* @since 1.21
*/
public final class RepositoryPermission
implements StringablePermission, Serializable
{
/**
* Type string of the permission
* @since 1.31
*/
public static final String TYPE = "repository";
/** Field description */
public static final String WILDCARD = "*";
/** Field description */
private static final long serialVersionUID = 3832804235417228043L;
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param repository
* @param permissionType
*/
public RepositoryPermission(Repository repository,
PermissionType permissionType)
{
this(repository.getId(), permissionType);
}
/**
* Constructs ...
*
*
* @param repositoryId
* @param permissionType
*/
public RepositoryPermission(String repositoryId,
PermissionType permissionType)
{
this.repositoryId = repositoryId;
this.permissionType = permissionType;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(repositoryId, other.repositoryId)
&& Objects.equal(permissionType, other.permissionType);
}
/**
* Method description
*
*
* @return
*/
@Override
public int hashCode()
{
return Objects.hashCode(repositoryId, permissionType);
}
/**
* Method description
*
*
* @param p
*
* @return
*/
@Override
public boolean implies(Permission p)
{
boolean result = false;
if (p instanceof RepositoryPermission)
{
RepositoryPermission rp = (RepositoryPermission) p;
//J-
result = (repositoryId.equals(WILDCARD) || repositoryId.equals(rp.repositoryId))
&& (permissionType.getValue() >= rp.permissionType.getValue());
//J+
}
return result;
}
/**
* Method description
*
*
* @return
*/
@Override
public String toString()
{
//J-
return MoreObjects.toStringHelper(this)
.add("repositoryId", repositoryId)
.add("permissionType", permissionType)
.toString();
//J+
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public String getAsString()
{
StringBuilder buffer = new StringBuilder(TYPE);
buffer.append(":").append(repositoryId).append(":").append(permissionType);
return buffer.toString();
}
/**
* Method description
*
*
* @return
*/
public PermissionType getPermissionType()
{
return permissionType;
}
/**
* Method description
*
*
* @return
*/
public String getRepositoryId()
{
return repositoryId;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private PermissionType permissionType;
/** Field description */
private String repositoryId;
}

View File

@@ -20,7 +20,7 @@ public class VndMediaType {
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX; public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
@@ -33,6 +33,7 @@ public class VndMediaType {
public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX;
public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX;
public static final String CONFIG = PREFIX + "config" + SUFFIX; public static final String CONFIG = PREFIX + "config" + SUFFIX;
public static final String REPOSITORY_PERMISSION_COLLECTION = PREFIX + "repositoryPermissionCollection" + SUFFIX;
public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX; public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;

View File

@@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
} }
else else
{ {
permitted = RepositoryPermissions.read(repository).isPermitted(); permitted = RepositoryPermissions.pull(repository).isPermitted();
} }
return permitted; return permitted;

View File

@@ -11,51 +11,51 @@ import java.util.Optional;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class LinkAppenderMapperTest { class HalAppenderMapperTest {
@Mock @Mock
private LinkAppender appender; private HalAppender appender;
private LinkEnricherRegistry registry; private HalEnricherRegistry registry;
private LinkAppenderMapper mapper; private HalAppenderMapper mapper;
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
registry = new LinkEnricherRegistry(); registry = new HalEnricherRegistry();
mapper = new LinkAppenderMapper(); mapper = new HalAppenderMapper();
mapper.setRegistry(registry); mapper.setRegistry(registry);
} }
@Test @Test
void shouldAppendSimpleLink() { void shouldAppendSimpleLink() {
registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com")); registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com"));
mapper.appendLinks(appender, "hello"); mapper.applyEnrichers(appender, "hello");
verify(appender).appendOne("42", "https://hitchhiker.com"); verify(appender).appendLink("42", "https://hitchhiker.com");
} }
@Test @Test
void shouldCallMultipleEnrichers() { void shouldCallMultipleEnrichers() {
registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com")); registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com"));
registry.register(String.class, (ctx, appender) -> appender.appendOne("21", "https://scm.hitchhiker.com")); registry.register(String.class, (ctx, appender) -> appender.appendLink("21", "https://scm.hitchhiker.com"));
mapper.appendLinks(appender, "hello"); mapper.applyEnrichers(appender, "hello");
verify(appender).appendOne("42", "https://hitchhiker.com"); verify(appender).appendLink("42", "https://hitchhiker.com");
verify(appender).appendOne("21", "https://scm.hitchhiker.com"); verify(appender).appendLink("21", "https://scm.hitchhiker.com");
} }
@Test @Test
void shouldAppendLinkByUsingSourceFromContext() { void shouldAppendLinkByUsingSourceFromContext() {
registry.register(String.class, (ctx, appender) -> { registry.register(String.class, (ctx, appender) -> {
Optional<String> rel = ctx.oneByType(String.class); Optional<String> rel = ctx.oneByType(String.class);
appender.appendOne(rel.get(), "https://hitchhiker.com"); appender.appendLink(rel.get(), "https://hitchhiker.com");
}); });
mapper.appendLinks(appender, "42"); mapper.applyEnrichers(appender, "42");
verify(appender).appendOne("42", "https://hitchhiker.com"); verify(appender).appendLink("42", "https://hitchhiker.com");
} }
@Test @Test
@@ -63,12 +63,12 @@ class LinkAppenderMapperTest {
registry.register(Integer.class, (ctx, appender) -> { registry.register(Integer.class, (ctx, appender) -> {
Optional<Integer> rel = ctx.oneByType(Integer.class); Optional<Integer> rel = ctx.oneByType(Integer.class);
Optional<String> href = ctx.oneByType(String.class); Optional<String> href = ctx.oneByType(String.class);
appender.appendOne(String.valueOf(rel.get()), href.get()); appender.appendLink(String.valueOf(rel.get()), href.get());
}); });
mapper.appendLinks(appender, Integer.valueOf(42), "https://hitchhiker.com"); mapper.applyEnrichers(appender, Integer.valueOf(42), "https://hitchhiker.com");
verify(appender).appendOne("42", "https://hitchhiker.com"); verify(appender).appendLink("42", "https://hitchhiker.com");
} }
} }

View File

@@ -7,17 +7,17 @@ import org.junit.jupiter.api.Test;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
class LinkEnricherContextTest { class HalEnricherContextTest {
@Test @Test
void shouldCreateContextFromSingleObject() { void shouldCreateContextFromSingleObject() {
LinkEnricherContext context = LinkEnricherContext.of("hello"); HalEnricherContext context = HalEnricherContext.of("hello");
assertThat(context.oneByType(String.class)).contains("hello"); assertThat(context.oneByType(String.class)).contains("hello");
} }
@Test @Test
void shouldCreateContextFromMultipleObjects() { void shouldCreateContextFromMultipleObjects() {
LinkEnricherContext context = LinkEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L)); HalEnricherContext context = HalEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L));
assertThat(context.oneByType(String.class)).contains("hello"); assertThat(context.oneByType(String.class)).contains("hello");
assertThat(context.oneByType(Integer.class)).contains(42); assertThat(context.oneByType(Integer.class)).contains(42);
assertThat(context.oneByType(Long.class)).contains(21L); assertThat(context.oneByType(Long.class)).contains(21L);
@@ -25,19 +25,19 @@ class LinkEnricherContextTest {
@Test @Test
void shouldReturnEmptyOptionalForUnknownTypes() { void shouldReturnEmptyOptionalForUnknownTypes() {
LinkEnricherContext context = LinkEnricherContext.of(); HalEnricherContext context = HalEnricherContext.of();
assertThat(context.oneByType(String.class)).isNotPresent(); assertThat(context.oneByType(String.class)).isNotPresent();
} }
@Test @Test
void shouldReturnRequiredObject() { void shouldReturnRequiredObject() {
LinkEnricherContext context = LinkEnricherContext.of("hello"); HalEnricherContext context = HalEnricherContext.of("hello");
assertThat(context.oneRequireByType(String.class)).isEqualTo("hello"); assertThat(context.oneRequireByType(String.class)).isEqualTo("hello");
} }
@Test @Test
void shouldThrowAnNoSuchElementExceptionForUnknownTypes() { void shouldThrowAnNoSuchElementExceptionForUnknownTypes() {
LinkEnricherContext context = LinkEnricherContext.of(); HalEnricherContext context = HalEnricherContext.of();
assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class)); assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class));
} }

View File

@@ -5,54 +5,54 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
class LinkEnricherRegistryTest { class HalEnricherRegistryTest {
private LinkEnricherRegistry registry; private HalEnricherRegistry registry;
@BeforeEach @BeforeEach
void setUpObjectUnderTest() { void setUpObjectUnderTest() {
registry = new LinkEnricherRegistry(); registry = new HalEnricherRegistry();
} }
@Test @Test
void shouldRegisterTheEnricher() { void shouldRegisterTheEnricher() {
SampleLinkEnricher enricher = new SampleLinkEnricher(); SampleHalEnricher enricher = new SampleHalEnricher();
registry.register(String.class, enricher); registry.register(String.class, enricher);
Iterable<LinkEnricher> enrichers = registry.allByType(String.class); Iterable<HalEnricher> enrichers = registry.allByType(String.class);
assertThat(enrichers).containsOnly(enricher); assertThat(enrichers).containsOnly(enricher);
} }
@Test @Test
void shouldRegisterMultipleEnrichers() { void shouldRegisterMultipleEnrichers() {
SampleLinkEnricher one = new SampleLinkEnricher(); SampleHalEnricher one = new SampleHalEnricher();
registry.register(String.class, one); registry.register(String.class, one);
SampleLinkEnricher two = new SampleLinkEnricher(); SampleHalEnricher two = new SampleHalEnricher();
registry.register(String.class, two); registry.register(String.class, two);
Iterable<LinkEnricher> enrichers = registry.allByType(String.class); Iterable<HalEnricher> enrichers = registry.allByType(String.class);
assertThat(enrichers).containsOnly(one, two); assertThat(enrichers).containsOnly(one, two);
} }
@Test @Test
void shouldRegisterEnrichersForDifferentTypes() { void shouldRegisterEnrichersForDifferentTypes() {
SampleLinkEnricher one = new SampleLinkEnricher(); SampleHalEnricher one = new SampleHalEnricher();
registry.register(String.class, one); registry.register(String.class, one);
SampleLinkEnricher two = new SampleLinkEnricher(); SampleHalEnricher two = new SampleHalEnricher();
registry.register(Integer.class, two); registry.register(Integer.class, two);
Iterable<LinkEnricher> enrichers = registry.allByType(String.class); Iterable<HalEnricher> enrichers = registry.allByType(String.class);
assertThat(enrichers).containsOnly(one); assertThat(enrichers).containsOnly(one);
enrichers = registry.allByType(Integer.class); enrichers = registry.allByType(Integer.class);
assertThat(enrichers).containsOnly(two); assertThat(enrichers).containsOnly(two);
} }
private static class SampleLinkEnricher implements LinkEnricher { private static class SampleHalEnricher implements HalEnricher {
@Override @Override
public void enrich(LinkEnricherContext context, LinkAppender appender) { public void enrich(HalEnricherContext context, HalAppender appender) {
} }
} }

View File

@@ -0,0 +1,49 @@
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
class RepositoryPermissionTest {
@Test
void shouldBeEqualWithSameVerbs() {
RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false);
RepositoryPermission permission2 = new RepositoryPermission("name", asList("two", "one"), false);
assertThat(permission1).isEqualTo(permission2);
}
@Test
void shouldHaveSameHashCodeWithSameVerbs() {
long hash1 = new RepositoryPermission("name", asList("one", "two"), false).hashCode();
long hash2 = new RepositoryPermission("name", asList("two", "one"), false).hashCode();
assertThat(hash1).isEqualTo(hash2);
}
@Test
void shouldNotBeEqualWithSameVerbs() {
RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false);
RepositoryPermission permission2 = new RepositoryPermission("name", asList("three", "one"), false);
assertThat(permission1).isNotEqualTo(permission2);
}
@Test
void shouldNotBeEqualWithDifferentType() {
RepositoryPermission permission1 = new RepositoryPermission("name", asList("one"), false);
RepositoryPermission permission2 = new RepositoryPermission("name", asList("one"), true);
assertThat(permission1).isNotEqualTo(permission2);
}
@Test
void shouldNotBeEqualWithDifferentName() {
RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one"), false);
RepositoryPermission permission2 = new RepositoryPermission("name2", asList("one"), false);
assertThat(permission1).isNotEqualTo(permission2);
}
}

View File

@@ -8,5 +8,5 @@ unpriv = secret
[roles] [roles]
admin = * admin = *
user = something:* user = something:*
repo_read = "repository:read:1" repo_read = "repository:read,pull:1"
repo_write = "repository:push:1" repo_write = "repository:read,write,pull,push:1"

View File

@@ -16,6 +16,7 @@ import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryTestData;
import java.io.IOException; import java.io.IOException;
@@ -24,8 +25,10 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Clock; import java.time.Clock;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -70,9 +73,7 @@ class XmlRepositoryDAOTest {
Clock clock = mock(Clock.class); Clock clock = mock(Clock.class);
when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); when(clock.millis()).then(ic -> atomicClock.incrementAndGet());
XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem, clock); return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
return dao;
} }
@Test @Test
@@ -329,6 +330,21 @@ class XmlRepositoryDAOTest {
assertThat(content).contains("Awesome Spaceship"); assertThat(content).contains("Awesome Spaceship");
} }
@Test
void shouldPersistPermissions() throws IOException {
Repository heartOfGold = createHeartOfGold();
heartOfGold.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", Collections.singletonList("delete"), true)));
dao.add(heartOfGold);
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
Path metadataPath = dao.resolveMetadataPath(repositoryDirectory);
String content = content(metadataPath);
System.out.println(content);
assertThat(content).containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>");
assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>");
}
@Test @Test
void shouldReadPathDatabaseAndMetadataOfRepositories() { void shouldReadPathDatabaseAndMetadataOfRepositories() {
Repository heartOfGold = createHeartOfGold(); Repository heartOfGold = createHeartOfGold();

View File

@@ -11,7 +11,8 @@
<groupId>sonia.scm</groupId> <groupId>sonia.scm</groupId>
<artifactId>scm-it</artifactId> <artifactId>scm-it</artifactId>
<packaging>jar</packaging> <!-- we need type war, because the jetty plugin does not work with jar or pom -->
<packaging>war</packaging>
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
<name>scm-it</name> <name>scm-it</name>
@@ -91,6 +92,14 @@
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>com.mycila.maven-license-plugin</groupId> <groupId>com.mycila.maven-license-plugin</groupId>
<artifactId>maven-license-plugin</artifactId> <artifactId>maven-license-plugin</artifactId>

View File

@@ -0,0 +1,209 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.it;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.it.utils.TestData;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static sonia.scm.it.utils.RestUtil.given;
/**
* Integration Tests for Git with non fast-forward pushes.
*/
public class GitNonFastForwardITCase {
private File workingCopy;
private Git git;
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Before
public void createAndCloneTestRepository() throws IOException, GitAPIException {
TestData.createDefault();
this.workingCopy = tempFolder.newFolder();
this.git = clone(RestUtil.BASE_URL.toASCIIString() + "repo/scmadmin/HeartOfGold-git");
}
@After
public void cleanup() {
TestData.cleanup();
}
/**
* Ensures that the normal behaviour (non fast-forward is allowed), is restored after the tests are executed.
*/
@AfterClass
public static void allowNonFastForward() {
setNonFastForwardDisallowed(false);
}
@Test
public void testGitPushAmendWithoutForce() throws IOException, GitAPIException {
setNonFastForwardDisallowed(false);
addTestFileToWorkingCopyAndCommit("a");
pushAndAssert(false, Status.OK);
addTestFileToWorkingCopyAndCommitAmend("c");
pushAndAssert(false, Status.REJECTED_NONFASTFORWARD);
}
@Test
public void testGitPushAmendWithForce() throws IOException, GitAPIException {
setNonFastForwardDisallowed(false);
addTestFileToWorkingCopyAndCommit("a");
pushAndAssert(false, Status.OK);
addTestFileToWorkingCopyAndCommitAmend("c");
pushAndAssert(true, Status.OK);
}
@Test
public void testGitPushAmendForceWithDisallowNonFastForward() throws GitAPIException, IOException {
setNonFastForwardDisallowed(true);
addTestFileToWorkingCopyAndCommit("a");
pushAndAssert(false, Status.OK);
addTestFileToWorkingCopyAndCommitAmend("c");
pushAndAssert(true, Status.REJECTED_OTHER_REASON);
setNonFastForwardDisallowed(false);
}
private CredentialsProvider createCredentialProvider() {
return new UsernamePasswordCredentialsProvider(
RestUtil.ADMIN_USERNAME, RestUtil.ADMIN_PASSWORD
);
}
private Git clone(String url) throws GitAPIException {
return Git.cloneRepository()
.setDirectory(workingCopy)
.setURI(url)
.setCredentialsProvider(createCredentialProvider())
.call();
}
private void addTestFileToWorkingCopyAndCommit(String name) throws IOException, GitAPIException {
addTestFile(name);
prepareCommit()
.setMessage("added ".concat(name))
.call();
}
private void addTestFile(String name) throws IOException, GitAPIException {
String filename = name.concat(".txt");
Files.write(name, new File(workingCopy, filename), Charsets.UTF_8);
git.add().addFilepattern(filename).call();
}
private CommitCommand prepareCommit() {
return git.commit()
.setAuthor("Trillian McMillian", "trillian@hitchhiker.com");
}
private void pushAndAssert(boolean force, Status expectedStatus) throws GitAPIException {
Iterable<PushResult> results = push(force);
assertStatus(results, expectedStatus);
}
private Iterable<PushResult> push(boolean force) throws GitAPIException {
return git.push()
.setRemote("origin")
.add("master")
.setForce(force)
.setCredentialsProvider(createCredentialProvider())
.call();
}
private void assertStatus(Iterable<PushResult> results, Status expectedStatus) {
for ( PushResult pushResult : results ) {
assertStatus(pushResult, expectedStatus);
}
}
private void assertStatus(PushResult pushResult, Status expectedStatus) {
for ( RemoteRefUpdate remoteRefUpdate : pushResult.getRemoteUpdates() ) {
assertEquals(expectedStatus, remoteRefUpdate.getStatus());
}
}
private void addTestFileToWorkingCopyAndCommitAmend(String name) throws IOException, GitAPIException {
addTestFile(name);
prepareCommit()
.setMessage("amend commit, because of missing ".concat(name))
.setAmend(true)
.call();
}
private static void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) {
String config = String.format("{'disabled': false, 'gcExpression': null, 'nonFastForwardDisallowed': %s}", nonFastForwardDisallowed)
.replace('\'', '"');
given(VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX)
.body(config)
.when()
.put(RestUtil.REST_BASE_URL.toASCIIString() + "config/git" )
.then()
.statusCode(HttpServletResponse.SC_NO_CONTENT);
}
}

View File

@@ -42,7 +42,6 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Parameterized.Parameters;
import sonia.scm.it.utils.RepositoryUtil; import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.TestData; import sonia.scm.it.utils.TestData;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -59,7 +58,10 @@ import static org.junit.Assert.assertNull;
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.utils.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes; import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.TestData.OWNER;
import static sonia.scm.it.utils.TestData.READ;
import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.utils.TestData.WRITE;
import static sonia.scm.it.utils.TestData.callRepository; import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
@@ -91,11 +93,11 @@ public class PermissionsITCase {
public void prepareEnvironment() { public void prepareEnvironment() {
TestData.createDefault(); TestData.createDefault();
TestData.createNotAdminUser(USER_READ, USER_PASS); TestData.createNotAdminUser(USER_READ, USER_PASS);
TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType); TestData.createUserPermission(USER_READ, READ, repositoryType);
TestData.createNotAdminUser(USER_WRITE, USER_PASS); TestData.createNotAdminUser(USER_WRITE, USER_PASS);
TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType); TestData.createUserPermission(USER_WRITE, WRITE, repositoryType);
TestData.createNotAdminUser(USER_OWNER, USER_PASS); TestData.createNotAdminUser(USER_OWNER, USER_PASS);
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); TestData.createUserPermission(USER_OWNER, OWNER, repositoryType);
TestData.createNotAdminUser(USER_OTHER, USER_PASS); TestData.createNotAdminUser(USER_OTHER, USER_PASS);
createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER); createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
} }
@@ -109,7 +111,7 @@ public class PermissionsITCase {
@Test @Test
public void readUserShouldNotSeeBruteForcePermissions() { public void readUserShouldNotSeeBruteForcePermissions() {
given(VndMediaType.PERMISSION, USER_READ, USER_PASS) given(VndMediaType.REPOSITORY_PERMISSION, USER_READ, USER_PASS)
.when() .when()
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
.then() .then()
@@ -125,7 +127,7 @@ public class PermissionsITCase {
@Test @Test
public void writeUserShouldNotSeeBruteForcePermissions() { public void writeUserShouldNotSeeBruteForcePermissions() {
given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS) given(VndMediaType.REPOSITORY_PERMISSION, USER_WRITE, USER_PASS)
.when() .when()
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
.then() .then()
@@ -145,7 +147,7 @@ public class PermissionsITCase {
@Test @Test
public void otherUserShouldNotSeeBruteForcePermissions() { public void otherUserShouldNotSeeBruteForcePermissions() {
given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS) given(VndMediaType.REPOSITORY_PERMISSION, USER_OTHER, USER_PASS)
.when() .when()
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
.then() .then()

View File

@@ -4,15 +4,16 @@ import io.restassured.response.ValidatableResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PermissionType;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonObjectBuilder; import javax.json.JsonObjectBuilder;
import java.net.URI; import java.net.URI;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static sonia.scm.it.utils.RestUtil.createResourceUrl; import static sonia.scm.it.utils.RestUtil.createResourceUrl;
@@ -25,6 +26,11 @@ public class TestData {
public static final String USER_SCM_ADMIN = "scmadmin"; public static final String USER_SCM_ADMIN = "scmadmin";
public static final String USER_ANONYMOUS = "anonymous"; public static final String USER_ANONYMOUS = "anonymous";
public static final Collection<String> READ = asList("read", "pull");
public static final Collection<String> WRITE = asList("read", "write", "pull", "push");
public static final Collection<String> OWNER = asList("*");
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>(); private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
@@ -82,13 +88,13 @@ public class TestData {
; ;
} }
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { public static void createUserPermission(String name, Collection<String> permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl); LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
given(VndMediaType.PERMISSION) given(VndMediaType.REPOSITORY_PERMISSION)
.when() .when()
.content("{\n" + .content("{\n" +
"\t\"type\": \"" + permissionType.name() + "\",\n" + "\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" +
"\t\"name\": \"" + name + "\",\n" + "\t\"name\": \"" + name + "\",\n" +
"\t\"groupPermission\": false\n" + "\t\"groupPermission\": false\n" +
"\t\n" + "\t\n" +
@@ -106,7 +112,7 @@ public class TestData {
} }
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) {
return given(VndMediaType.PERMISSION, username, password) return given(VndMediaType.REPOSITORY_PERMISSION, username, password)
.when() .when()
.get(TestData.getDefaultPermissionUrl(username, password, repositoryType)) .get(TestData.getDefaultPermissionUrl(username, password, repositoryType))
.then() .then()

View File

@@ -82,7 +82,6 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -15,6 +15,8 @@ public class GitConfigDto extends HalRepresentation {
private String gcExpression; private String gcExpression;
private boolean nonFastForwardDisallowed;
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) { protected HalRepresentation add(Links links) {

View File

@@ -55,8 +55,10 @@ public class GitConfig extends RepositoryConfig {
@XmlElement(name = "gc-expression") @XmlElement(name = "gc-expression")
private String gcExpression; private String gcExpression;
public String getGcExpression() @XmlElement(name = "disallow-non-fast-forward")
{ private boolean nonFastForwardDisallowed;
public String getGcExpression() {
return gcExpression; return gcExpression;
} }
@@ -64,6 +66,14 @@ public class GitConfig extends RepositoryConfig {
this.gcExpression = gcExpression; this.gcExpression = gcExpression;
} }
public boolean isNonFastForwardDisallowed() {
return nonFastForwardDisallowed;
}
public void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) {
this.nonFastForwardDisallowed = nonFastForwardDisallowed;
}
@Override @Override
@XmlTransient // Only for permission checks, don't serialize to XML @XmlTransient // Only for permission checks, don't serialize to XML
public String getId() { public String getId() {

View File

@@ -68,10 +68,13 @@ public class GitHookTagProvider implements HookTagProvider {
if (Strings.isNullOrEmpty(tag)){ if (Strings.isNullOrEmpty(tag)){
logger.debug("received ref name {} is not a tag", refName); logger.debug("received ref name {} is not a tag", refName);
} else if (rc.getType() == ReceiveCommand.Type.CREATE) { } else if (isCreate(rc)) {
createdTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getNewId()))); createdTagBuilder.add(createTagFromNewId(rc, tag));
} else if (rc.getType() == ReceiveCommand.Type.DELETE){ } else if (isDelete(rc)){
deletedTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getOldId()))); deletedTagBuilder.add(createTagFromOldId(rc, tag));
} else if (isUpdate(rc)) {
createdTagBuilder.add(createTagFromNewId(rc, tag));
deletedTagBuilder.add(createTagFromOldId(rc, tag));
} }
} }
@@ -79,6 +82,26 @@ public class GitHookTagProvider implements HookTagProvider {
deletedTags = deletedTagBuilder.build(); deletedTags = deletedTagBuilder.build();
} }
private Tag createTagFromNewId(ReceiveCommand rc, String tag) {
return new Tag(tag, GitUtil.getId(rc.getNewId()));
}
private Tag createTagFromOldId(ReceiveCommand rc, String tag) {
return new Tag(tag, GitUtil.getId(rc.getOldId()));
}
private boolean isUpdate(ReceiveCommand rc) {
return rc.getType() == ReceiveCommand.Type.UPDATE || rc.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
}
private boolean isDelete(ReceiveCommand rc) {
return rc.getType() == ReceiveCommand.Type.DELETE;
}
private boolean isCreate(ReceiveCommand rc) {
return rc.getType() == ReceiveCommand.Type.CREATE;
}
@Override @Override
public List<Tag> getCreatedTags() { public List<Tag> getCreatedTags() {
return createdTags; return createdTags;

View File

@@ -35,79 +35,63 @@ package sonia.scm.web;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject; import com.google.inject.Inject;
import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory; import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
//~--- JDK imports ------------------------------------------------------------
/** /**
* GitReceivePackFactory creates {@link ReceivePack} objects and assigns the required
* Hook components.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitReceivePackFactory public class GitReceivePackFactory implements ReceivePackFactory<HttpServletRequest>
implements ReceivePackFactory<HttpServletRequest>
{ {
/** private final GitRepositoryHandler handler;
* Constructs ...
* private ReceivePackFactory wrapped;
*
* private final GitReceiveHook hook;
* @param hookEventFacade
* @param handler
*/
@Inject @Inject
public GitReceivePackFactory(HookEventFacade hookEventFacade, public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
GitRepositoryHandler handler) this.handler = handler;
{ this.hook = new GitReceiveHook(hookEventFacade, handler);
hook = new GitReceiveHook(hookEventFacade, handler); this.wrapped = new DefaultReceivePackFactory();
} }
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param repository
*
* @return
*
* @throws ServiceNotAuthorizedException
* @throws ServiceNotEnabledException
*/
@Override @Override
public ReceivePack create(HttpServletRequest request, Repository repository) public ReceivePack create(HttpServletRequest request, Repository repository)
throws ServiceNotEnabledException, ServiceNotAuthorizedException throws ServiceNotEnabledException, ServiceNotAuthorizedException {
{ ReceivePack receivePack = wrapped.create(request, repository);
ReceivePack rpack = defaultFactory.create(request, repository); receivePack.setAllowNonFastForwards(isNonFastForwardAllowed());
rpack.setPreReceiveHook(hook); receivePack.setPreReceiveHook(hook);
rpack.setPostReceiveHook(hook); receivePack.setPostReceiveHook(hook);
// apply collecting listener, to be able to check which commits are new // apply collecting listener, to be able to check which commits are new
CollectingPackParserListener.set(rpack); CollectingPackParserListener.set(receivePack);
return rpack; return receivePack;
} }
//~--- fields --------------------------------------------------------------- private boolean isNonFastForwardAllowed() {
return ! handler.getConfig().isNonFastForwardDisallowed();
}
/** Field description */ @VisibleForTesting
private DefaultReceivePackFactory defaultFactory = void setWrapped(ReceivePackFactory wrapped) {
new DefaultReceivePackFactory(); this.wrapped = wrapped;
}
/** Field description */
private GitReceiveHook hook;
} }

View File

@@ -8,6 +8,7 @@ import { InputField, Checkbox } from "@scm-manager/ui-components";
type Configuration = { type Configuration = {
repositoryDirectory?: string, repositoryDirectory?: string,
gcExpression?: string, gcExpression?: string,
nonFastForwardDisallowed: boolean,
disabled: boolean, disabled: boolean,
_links: Links _links: Links
} }
@@ -41,7 +42,7 @@ class GitConfigurationForm extends React.Component<Props, State> {
}; };
render() { render() {
const { gcExpression, disabled } = this.state; const { gcExpression, nonFastForwardDisallowed, disabled } = this.state;
const { readOnly, t } = this.props; const { readOnly, t } = this.props;
return ( return (
@@ -53,6 +54,13 @@ class GitConfigurationForm extends React.Component<Props, State> {
onChange={this.handleChange} onChange={this.handleChange}
disabled={readOnly} disabled={readOnly}
/> />
<Checkbox name="nonFastForwardDisallowed"
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
checked={nonFastForwardDisallowed}
onChange={this.handleChange}
disabled={readOnly}
/>
<Checkbox name="disabled" <Checkbox name="disabled"
label={t("scm-git-plugin.config.disabled")} label={t("scm-git-plugin.config.disabled")}
helpText={t("scm-git-plugin.config.disabledHelpText")} helpText={t("scm-git-plugin.config.disabledHelpText")}

View File

@@ -1,9 +1,55 @@
{ {
"scm-git-plugin": { "scm-git-plugin": {
"information": { "information": {
"clone" : "Repository Klonen", "clone" : "Repository klonen",
"create" : "Neue Repository erstellen", "create" : "Neues Repository erstellen",
"replace" : "Eine existierende Repository aktualisieren" "replace" : "Ein bestehendes Repository aktualisieren",
"merge": {
"heading": "Merge des Source Branch in den Target Branch",
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",
"update": "2. Update Workspace",
"merge": "3. Merge Source Branch",
"resolve": "4. Merge Konflikte auflösen und korrigierte Dateien dem Index hinzufügen.",
"commit": "5. Commit",
"push": "6. Push des Merge"
}
},
"config": {
"link": "Git",
"title": "Git Konfiguration",
"gcExpression": "GC Cron Ausdruck",
"gcExpressionHelpText": "Benutze Quartz Cron Ausdrücke (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK), um git GC regelmäßig auszuführen.",
"nonFastForwardDisallowed": "Deaktiviere \"Non Fast-Forward\"",
"nonFastForwardDisallowedHelpText": "Git Pushes ablehnen, die nicht \"fast-forward\" sind, wie \"--force\".",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin",
"submit": "Speichern"
},
"repo-config": {
"link": "Konfiguration",
"default-branch": "Standard Branch",
"submit": "Speichern",
"error": {
"title": "Fehler",
"subtitle": "Ein Fehler ist aufgetreten."
},
"success": "Der standard Branch wurde geändert!"
}
},
"permissions" : {
"configuration": {
"read": {
"git": {
"displayName": "Git Konfiguration lesen",
"description": "Darf die git Konfiguration lesen."
}
},
"write": {
"git": {
"displayName": "Git Konfiguration schreiben",
"description": "Darf die git Konfiguration verändern."
}
}
} }
} }
} }

View File

@@ -19,6 +19,8 @@
"title": "Git Configuration", "title": "Git Configuration",
"gcExpression": "GC Cron Expression", "gcExpression": "GC Cron Expression",
"gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.", "gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.",
"nonFastForwardDisallowed": "Disallow Non Fast-Forward",
"nonFastForwardDisallowedHelpText": "Reject git pushes which are non fast-forward such as --force.",
"disabled": "Disabled", "disabled": "Disabled",
"disabledHelpText": "Enable or disable the Git plugin", "disabledHelpText": "Enable or disable the Git plugin",
"submit": "Submit" "submit": "Submit"

View File

@@ -6,8 +6,7 @@ import org.mockito.InjectMocks;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig; import sonia.scm.repository.GitConfig;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class GitConfigDtoToGitConfigMapperTest { public class GitConfigDtoToGitConfigMapperTest {
@@ -21,12 +20,14 @@ public class GitConfigDtoToGitConfigMapperTest {
GitConfig config = mapper.map(dto); GitConfig config = mapper.map(dto);
assertEquals("express", config.getGcExpression()); assertEquals("express", config.getGcExpression());
assertFalse(config.isDisabled()); assertFalse(config.isDisabled());
assertTrue(config.isNonFastForwardDisallowed());
} }
private GitConfigDto createDefaultDto() { private GitConfigDto createDefaultDto() {
GitConfigDto gitConfigDto = new GitConfigDto(); GitConfigDto gitConfigDto = new GitConfigDto();
gitConfigDto.setGcExpression("express"); gitConfigDto.setGcExpression("express");
gitConfigDto.setDisabled(false); gitConfigDto.setDisabled(false);
gitConfigDto.setNonFastForwardDisallowed(true);
return gitConfigDto; return gitConfigDto;
} }
} }

View File

@@ -32,20 +32,23 @@
package sonia.scm.repository.api; package sonia.scm.repository.api;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.util.List;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.OngoingStubbing;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import java.util.List;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
/** /**
* Unit tests for {@link GitHookTagProvider}. * Unit tests for {@link GitHookTagProvider}.
* *
@@ -54,6 +57,11 @@ import sonia.scm.repository.Tag;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class GitHookTagProviderTest { public class GitHookTagProviderTest {
private static final String ZERO = ObjectId.zeroId().getName();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock @Mock
private ReceiveCommand command; private ReceiveCommand command;
@@ -73,7 +81,7 @@ public class GitHookTagProviderTest {
@Test @Test
public void testGetCreatedTags() { public void testGetCreatedTags() {
String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7";
GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0", revision); GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/tags/1.0.0", revision, ZERO);
assertTag("1.0.0", revision, provider.getCreatedTags()); assertTag("1.0.0", revision, provider.getCreatedTags());
assertThat(provider.getDeletedTags(), empty()); assertThat(provider.getDeletedTags(), empty());
@@ -85,7 +93,7 @@ public class GitHookTagProviderTest {
@Test @Test
public void testGetDeletedTags() { public void testGetDeletedTags() {
String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7";
GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, "refs/tags/1.0.0", revision); GitHookTagProvider provider = createProvider(ReceiveCommand.Type.DELETE, "refs/tags/1.0.0", ZERO, revision);
assertThat(provider.getCreatedTags(), empty()); assertThat(provider.getCreatedTags(), empty());
assertTag("1.0.0", revision, provider.getDeletedTags()); assertTag("1.0.0", revision, provider.getDeletedTags());
@@ -97,12 +105,25 @@ public class GitHookTagProviderTest {
@Test @Test
public void testWithBranch(){ public void testWithBranch(){
String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7"; String revision = "b2002b64013e54b78eac251df0672bd5d6a83aa7";
GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/heads/1.0.0", revision); GitHookTagProvider provider = createProvider(ReceiveCommand.Type.CREATE, "refs/heads/1.0.0", revision, revision);
assertThat(provider.getCreatedTags(), empty()); assertThat(provider.getCreatedTags(), empty());
assertThat(provider.getDeletedTags(), empty()); assertThat(provider.getDeletedTags(), empty());
} }
/**
* Tests {@link GitHookTagProvider} with update command.
*/
@Test
public void testUpdateTags() {
String newId = "b2002b64013e54b78eac251df0672bd5d6a83aa7";
String oldId = "e0f2be968b147ff7043684a7715d2fe852553db4";
GitHookTagProvider provider = createProvider(ReceiveCommand.Type.UPDATE, "refs/tags/1.0.0", newId, oldId);
assertTag("1.0.0", newId, provider.getCreatedTags());
assertTag("1.0.0", oldId, provider.getDeletedTags());
}
private void assertTag(String name, String revision, List<Tag> tags){ private void assertTag(String name, String revision, List<Tag> tags){
assertNotNull(tags); assertNotNull(tags);
assertFalse(tags.isEmpty()); assertFalse(tags.isEmpty());
@@ -112,18 +133,11 @@ public class GitHookTagProviderTest {
assertEquals(revision, tag.getRevision()); assertEquals(revision, tag.getRevision());
} }
private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String id){ private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String newId, String oldId){
OngoingStubbing<ObjectId> ongoing; when(command.getNewId()).thenReturn(ObjectId.fromString(newId));
if (type == ReceiveCommand.Type.CREATE){ when(command.getOldId()).thenReturn(ObjectId.fromString(oldId));
ongoing = when(command.getNewId());
} else {
ongoing = when(command.getOldId());
}
ongoing.thenReturn(ObjectId.fromString(id));
when(command.getType()).thenReturn(type); when(command.getType()).thenReturn(type);
when(command.getRefName()).thenReturn(ref); when(command.getRefName()).thenReturn(ref);
return new GitHookTagProvider(commands); return new GitHookTagProvider(commands);
} }

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link GitReceivePackFactory}.
*/
@RunWith(MockitoJUnitRunner.class)
public class GitReceivePackFactoryTest {
@Mock
private GitRepositoryHandler handler;
private GitConfig config;
@Mock
private ReceivePackFactory wrappedReceivePackFactory;
private GitReceivePackFactory factory;
@Mock
private HttpServletRequest request;
private Repository repository;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void setUpObjectUnderTest() throws Exception {
this.repository = createRepositoryForTesting();
config = new GitConfig();
when(handler.getConfig()).thenReturn(config);
ReceivePack receivePack = new ReceivePack(repository);
when(wrappedReceivePackFactory.create(request, repository)).thenReturn(receivePack);
factory = new GitReceivePackFactory(handler, null);
factory.setWrapped(wrappedReceivePackFactory);
}
private Repository createRepositoryForTesting() throws GitAPIException, IOException {
File directory = temporaryFolder.newFolder();
return Git.init().setDirectory(directory).call().getRepository();
}
@Test
public void testCreate() throws Exception {
ReceivePack receivePack = factory.create(request, repository);
assertThat(receivePack.getPackParserListener(), instanceOf(CollectingPackParserListener.class));
assertThat(receivePack.getPreReceiveHook(), instanceOf(GitReceiveHook.class));
assertThat(receivePack.getPostReceiveHook(), instanceOf(GitReceiveHook.class));
assertTrue(receivePack.isAllowNonFastForwards());
}
@Test
public void testCreateWithDisabledNonFastForward() throws Exception {
config.setNonFastForwardDisallowed(true);
ReceivePack receivePack = factory.create(request, repository);
assertFalse(receivePack.isAllowNonFastForwards());
}
}

View File

@@ -20,7 +20,7 @@
<dependency> <dependency>
<groupId>com.aragost.javahg</groupId> <groupId>com.aragost.javahg</groupId>
<artifactId>javahg</artifactId> <artifactId>javahg</artifactId>
<version>0.8-scm1</version> <version>0.13-java7</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
@@ -81,7 +81,6 @@
</executions> </executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@@ -93,19 +92,6 @@
<url>http://maven.scm-manager.org/nexus/content/groups/public</url> <url>http://maven.scm-manager.org/nexus/content/groups/public</url>
</repository> </repository>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>sonatype-ossrh</id>
<name>Sonatype Open Source Software Repository Hosting</name>
<layout>default</layout>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories> </repositories>
</project> </project>

View File

@@ -19,6 +19,8 @@ public class HgConfigDto extends HalRepresentation {
private String pythonPath; private String pythonPath;
private boolean useOptimizedBytecode; private boolean useOptimizedBytecode;
private boolean showRevisionInId; private boolean showRevisionInId;
private boolean enableHttpPostArgs;
private boolean disableHookSSLValidation;
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -58,6 +58,14 @@ public class HgConfig extends RepositoryConfig
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@Override
@XmlTransient // Only for permission checks, don't serialize to XML
public String getId() {
// Don't change this without migrating SCM permission configuration!
return PERMISSION;
}
/** /**
* Method description * Method description
* *
@@ -124,6 +132,14 @@ public class HgConfig extends RepositoryConfig
return useOptimizedBytecode; return useOptimizedBytecode;
} }
public boolean isDisableHookSSLValidation() {
return disableHookSSLValidation;
}
public boolean isEnableHttpPostArgs() {
return enableHttpPostArgs;
}
/** /**
* Method description * Method description
* *
@@ -194,6 +210,10 @@ public class HgConfig extends RepositoryConfig
this.showRevisionInId = showRevisionInId; this.showRevisionInId = showRevisionInId;
} }
public void setEnableHttpPostArgs(boolean enableHttpPostArgs) {
this.enableHttpPostArgs = enableHttpPostArgs;
}
/** /**
* Method description * Method description
* *
@@ -205,6 +225,10 @@ public class HgConfig extends RepositoryConfig
this.useOptimizedBytecode = useOptimizedBytecode; this.useOptimizedBytecode = useOptimizedBytecode;
} }
public void setDisableHookSSLValidation(boolean disableHookSSLValidation) {
this.disableHookSSLValidation = disableHookSSLValidation;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
@@ -225,10 +249,11 @@ public class HgConfig extends RepositoryConfig
/** Field description */ /** Field description */
private boolean showRevisionInId = false; private boolean showRevisionInId = false;
@Override private boolean enableHttpPostArgs = false;
@XmlTransient // Only for permission checks, don't serialize to XML
public String getId() { /**
// Don't change this without migrating SCM permission configuration! * disable validation of ssl certificates for mercurial hook
return PERMISSION; * @see <a href="https://goo.gl/zH5eY8">Issue 959</a>
} */
private boolean disableHookSSLValidation = false;
} }

View File

@@ -56,15 +56,20 @@ import sonia.scm.web.cgi.CGIExecutor;
import sonia.scm.web.cgi.CGIExecutorFactory; import sonia.scm.web.cgi.CGIExecutorFactory;
import sonia.scm.web.cgi.EnvList; import sonia.scm.web.cgi.EnvList;
//~--- JDK imports ------------------------------------------------------------
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Map;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
import java.util.Base64; import java.util.Base64;
import java.util.Enumeration;
/** /**
* *
@@ -74,6 +79,8 @@ import java.util.Enumeration;
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
{ {
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
/** Field description */ /** Field description */
public static final String ENV_REPOSITORY_NAME = "REPO_NAME"; public static final String ENV_REPOSITORY_NAME = "REPO_NAME";
@@ -83,6 +90,8 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
/** Field description */ /** Field description */
public static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID"; public static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
/** Field description */ /** Field description */
public static final String ENV_SESSION_PREFIX = "SCM_"; public static final String ENV_SESSION_PREFIX = "SCM_";
@@ -268,9 +277,20 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
directory.getAbsolutePath()); directory.getAbsolutePath());
// add hook environment // add hook environment
Map<String, String> environment = executor.getEnvironment().asMutableMap();
if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation
// Issue 959: https://goo.gl/zH5eY8
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
}
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
//J- //J-
HgEnvironment.prepareEnvironment( HgEnvironment.prepareEnvironment(
executor.getEnvironment().asMutableMap(), environment,
handler, handler,
hookManager, hookManager,
request request

View File

@@ -33,13 +33,23 @@
package sonia.scm.web; package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.web.filter.PermissionFilter; import sonia.scm.web.filter.PermissionFilter;
import sonia.scm.repository.HgRepositoryHandler;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/** /**
* Permission filter for mercurial repositories. * Permission filter for mercurial repositories.
@@ -51,14 +61,48 @@ public class HgPermissionFilter extends PermissionFilter
private static final Set<String> READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); private static final Set<String> READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE");
public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) private final HgRepositoryHandler repositoryHandler;
public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate, HgRepositoryHandler repositoryHandler)
{ {
super(configuration, delegate); super(configuration, delegate);
this.repositoryHandler = repositoryHandler;
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws IOException, ServletException {
super.service(wrapRequestIfRequired(request), response, repository);
}
@VisibleForTesting
HttpServletRequest wrapRequestIfRequired(HttpServletRequest request) {
if (isHttpPostArgsEnabled()) {
return new HgServletRequest(request);
}
return request;
} }
@Override @Override
public boolean isWriteRequest(HttpServletRequest request) public boolean isWriteRequest(HttpServletRequest request)
{ {
return !READ_METHODS.contains(request.getMethod()); if (isHttpPostArgsEnabled()) {
return isHttpPostArgsWriteRequest(request);
}
return isDefaultWriteRequest(request);
}
private boolean isHttpPostArgsEnabled() {
return repositoryHandler.getConfig().isEnableHttpPostArgs();
}
private boolean isHttpPostArgsWriteRequest(HttpServletRequest request) {
return WireProtocol.isWriteRequest(request);
}
private boolean isDefaultWriteRequest(HttpServletRequest request) {
if (READ_METHODS.contains(request.getMethod())) {
return WireProtocol.isWriteRequest(request);
}
return true;
} }
} }

View File

@@ -12,10 +12,12 @@ import javax.inject.Inject;
public class HgPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { public class HgPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory {
private final ScmConfiguration configuration; private final ScmConfiguration configuration;
private final HgRepositoryHandler repositoryHandler;
@Inject @Inject
public HgPermissionFilterFactory(ScmConfiguration configuration) { public HgPermissionFilterFactory(ScmConfiguration configuration, HgRepositoryHandler repositoryHandler) {
this.configuration = configuration; this.configuration = configuration;
this.repositoryHandler = repositoryHandler;
} }
@Override @Override
@@ -25,6 +27,6 @@ public class HgPermissionFilterFactory implements ScmProviderHttpServletDecorato
@Override @Override
public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) {
return new HgPermissionFilter(configuration, delegate); return new HgPermissionFilter(configuration, delegate,repositoryHandler);
} }
} }

View File

@@ -0,0 +1,55 @@
package sonia.scm.web;
import com.google.common.base.Preconditions;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* HgServletInputStream is a wrapper around the original {@link ServletInputStream} and provides some extra
* functionality to support the mercurial client.
*/
public class HgServletInputStream extends ServletInputStream {
private final ServletInputStream original;
private ByteArrayInputStream captured;
HgServletInputStream(ServletInputStream original) {
this.original = original;
}
/**
* Reads the given amount of bytes from the stream and captures them, if the {@link #read()} methods is called the
* captured bytes are returned before the rest of the stream.
*
* @param size amount of bytes to read
*
* @return byte array
*
* @throws IOException if the method is called twice
*/
public byte[] readAndCapture(int size) throws IOException {
Preconditions.checkState(captured == null, "readAndCapture can only be called once per request");
// TODO should we enforce a limit? to prevent OOM?
byte[] bytes = new byte[size];
original.read(bytes);
captured = new ByteArrayInputStream(bytes);
return bytes;
}
@Override
public int read() throws IOException {
if (captured != null && captured.available() > 0) {
return captured.read();
}
return original.read();
}
@Override
public void close() throws IOException {
original.close();
}
}

View File

@@ -0,0 +1,31 @@
package sonia.scm.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
/**
* {@link HttpServletRequestWrapper} which adds some functionality in order to support the mercurial client.
*/
public final class HgServletRequest extends HttpServletRequestWrapper {
private HgServletInputStream hgServletInputStream;
/**
* Constructs a request object wrapping the given request.
*
* @param request
* @throws IllegalArgumentException if the request is null
*/
public HgServletRequest(HttpServletRequest request) {
super(request);
}
@Override
public HgServletInputStream getInputStream() throws IOException {
if (hgServletInputStream == null) {
hgServletInputStream = new HgServletInputStream(super.getInputStream());
}
return hgServletInputStream;
}
}

View File

@@ -0,0 +1,236 @@
/**
* Copyright (c) 2018, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.util.HttpUtil;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;
/**
* WireProtocol provides methods for handling the mercurial wire protocol.
*
* @see <a href="https://goo.gl/WaVJzw">Mercurial Wire Protocol</a>
*/
public final class WireProtocol {
private static final Logger LOG = LoggerFactory.getLogger(WireProtocol.class);
private static final Set<String> READ_COMMANDS = ImmutableSet.of(
"batch", "between", "branchmap", "branches", "capabilities", "changegroup", "changegroupsubset", "clonebundles",
"getbundle", "heads", "hello", "listkeys", "lookup", "known", "stream_out",
// could not find lheads in the wireprotocol description but mercurial 4.5.2 uses it for clone
"lheads"
);
private static final Set<String> WRITE_COMMANDS = ImmutableSet.of(
"pushkey", "unbundle"
);
private WireProtocol() {
}
/**
* Returns {@code true} if the request is a write request. The method will always return {@code true}, expect for the
* following cases:
*
* - no command was specified with the request (is required for the hgweb ui)
* - the command in the query string was found in the list of read request
* - if query string contains the batch command, then all commands specified in X-HgArg headers must be
* in the list of read requests
* - in case of enabled HttpPostArgs protocol and query string container the batch command, the header X-HgArgs-Post
* is read and the commands which are specified in the body from 0 to the value of X-HgArgs-Post must be in the list
* of read requests
*
* @param request http request
*
* @return {@code true} for write requests.
*/
public static boolean isWriteRequest(HttpServletRequest request) {
List<String> commands = commandsOf(request);
boolean write = isWriteRequest(commands);
LOG.trace("mercurial request {} is write: {}", commands, write);
return write;
}
@VisibleForTesting
static boolean isWriteRequest(List<String> commands) {
return !READ_COMMANDS.containsAll(commands);
}
@VisibleForTesting
static List<String> commandsOf(HttpServletRequest request) {
List<String> listOfCmds = Lists.newArrayList();
String cmd = getCommandFromQueryString(request);
if (cmd != null) {
listOfCmds.add(cmd);
if (isBatchCommand(cmd)) {
parseHgArgHeaders(request, listOfCmds);
handleHttpPostArgs(request, listOfCmds);
}
}
return Collections.unmodifiableList(listOfCmds);
}
private static void handleHttpPostArgs(HttpServletRequest request, List<String> listOfCmds) {
int hgArgsPostSize = request.getIntHeader("X-HgArgs-Post");
if (hgArgsPostSize > 0) {
if (request instanceof HgServletRequest) {
HgServletRequest hgRequest = (HgServletRequest) request;
parseHttpPostArgs(listOfCmds, hgArgsPostSize, hgRequest);
} else {
throw new IllegalArgumentException("could not process the httppostargs protocol without HgServletRequest");
}
}
}
private static void parseHttpPostArgs(List<String> listOfCmds, int hgArgsPostSize, HgServletRequest hgRequest) {
try {
byte[] bytes = hgRequest.getInputStream().readAndCapture(hgArgsPostSize);
// we use iso-8859-1 for encoding, because the post args are normally http headers which are using iso-8859-1
// see https://tools.ietf.org/html/rfc7230#section-3.2.4
String hgArgs = new String(bytes, Charsets.ISO_8859_1);
String decoded = decodeValue(hgArgs);
parseHgCommandHeader(listOfCmds, decoded);
} catch (IOException ex) {
throw Throwables.propagate(ex);
}
}
private static void parseHgArgHeaders(HttpServletRequest request, List<String> listOfCmds) {
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = (String) headerNames.nextElement();
parseHgArgHeader(request, listOfCmds, header);
}
}
private static void parseHgArgHeader(HttpServletRequest request, List<String> listOfCmds, String header) {
if (isHgArgHeader(header)) {
String value = getHeaderDecoded(request, header);
parseHgArgValue(listOfCmds, value);
}
}
private static void parseHgArgValue(List<String> listOfCmds, String value) {
if (isHgArgCommandHeader(value)) {
parseHgCommandHeader(listOfCmds, value);
}
}
private static void parseHgCommandHeader(List<String> listOfCmds, String value) {
String[] cmds = value.substring(5).split(";");
for (String cmd : cmds ) {
String normalizedCmd = normalize(cmd);
int index = normalizedCmd.indexOf(' ');
if (index > 0) {
listOfCmds.add(normalizedCmd.substring(0, index));
} else {
listOfCmds.add(normalizedCmd);
}
}
}
private static String normalize(String cmd) {
return cmd.trim().toLowerCase(Locale.ENGLISH);
}
private static boolean isHgArgCommandHeader(String value) {
return value.startsWith("cmds=");
}
private static String getHeaderDecoded(HttpServletRequest request, String header) {
return decodeValue(request.getHeader(header));
}
private static String decodeValue(String value) {
return HttpUtil.decode(Strings.nullToEmpty(value));
}
private static boolean isHgArgHeader(String header) {
return header.toLowerCase(Locale.ENGLISH).startsWith("x-hgarg-");
}
private static boolean isBatchCommand(String cmd) {
return "batch".equalsIgnoreCase(cmd);
}
private static String getCommandFromQueryString(HttpServletRequest request) {
// we can't use getParameter, because this would inspect the body for form parameters as well
Multimap<String, String> queryParameterMap = createQueryParameterMap(request);
Collection<String> cmd = queryParameterMap.get("cmd");
Preconditions.checkArgument(cmd.size() <= 1, "found more than one cmd query parameter");
Iterator<String> iterator = cmd.iterator();
String command = null;
if (iterator.hasNext()) {
command = iterator.next();
}
return command;
}
private static Multimap<String,String> createQueryParameterMap(HttpServletRequest request) {
Multimap<String,String> parameterMap = HashMultimap.create();
String queryString = request.getQueryString();
if (!Strings.isNullOrEmpty(queryString)) {
String[] parameters = queryString.split("&");
for (String parameter : parameters) {
int index = parameter.indexOf('=');
if (index > 0) {
parameterMap.put(parameter.substring(0, index), parameter.substring(index + 1));
} else {
parameterMap.put(parameter, "true");
}
}
}
return parameterMap;
}
}

View File

@@ -11,6 +11,8 @@ type Configuration = {
"encoding": string, "encoding": string,
"useOptimizedBytecode": boolean, "useOptimizedBytecode": boolean,
"showRevisionInId": boolean, "showRevisionInId": boolean,
"disableHookSSLValidation": boolean,
"enableHttpPostArgs": boolean,
"disabled": boolean, "disabled": boolean,
"_links": Links "_links": Links
}; };
@@ -101,6 +103,8 @@ class HgConfigurationForm extends React.Component<Props, State> {
{this.inputField("encoding")} {this.inputField("encoding")}
{this.checkbox("useOptimizedBytecode")} {this.checkbox("useOptimizedBytecode")}
{this.checkbox("showRevisionInId")} {this.checkbox("showRevisionInId")}
{this.checkbox("disableHookSSLValidation")}
{this.checkbox("enableHttpPostArgs")}
{this.checkbox("disabled")} {this.checkbox("disabled")}
</> </>
); );

View File

@@ -1,9 +1,48 @@
{ {
"scm-hg-plugin": { "scm-hg-plugin": {
"information": { "information": {
"clone" : "Repository Klonen", "clone" : "Repository klonen",
"create" : "Neue Repository erstellen", "create" : "Neues Repository erstellen",
"replace" : "Eine existierende Repository aktualisieren" "replace" : "Ein bestehendes Repository aktualisieren"
},
"config": {
"link": "Mercurial",
"title": "Mercurial Konfiguration",
"hgBinary": "HG Binary",
"hgBinaryHelpText": "Pfad des Mercurial Binary.",
"pythonBinary": "Python Binary",
"pythonBinaryHelpText": "Pfad des Python binary.",
"pythonPath": "Python Module Such Pfad",
"pythonPathHelpText": "Python Module Such Pfad (PYTHONPATH).",
"encoding": "Encoding",
"encodingHelpText": "Repository Encoding.",
"useOptimizedBytecode": "Optimized Bytecode (.pyo)",
"useOptimizedBytecodeHelpText": "Verwende den Python '-O' Switch.",
"showRevisionInId": "Revision anzeigen",
"showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.",
"enableHttpPostArgs": "HttpPostArgs Protocol aktivieren",
"enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.",
"disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren",
"disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.",
"required": "Dieser Konfigurationswert wird benötigt"
}
},
"permissions" : {
"configuration": {
"read": {
"hg": {
"displayName": "Mercurial Konfiguration lesen",
"description": "Darf die Mercurial Konfiguration lesen"
}
},
"write": {
"hg": {
"displayName": "Mercurial Konfiguration schreiben",
"description": "Darf die Mercurial Konfiguration verändern"
}
}
} }
} }
} }

View File

@@ -20,6 +20,10 @@
"useOptimizedBytecodeHelpText": "Use the Python '-O' switch.", "useOptimizedBytecodeHelpText": "Use the Python '-O' switch.",
"showRevisionInId": "Show Revision", "showRevisionInId": "Show Revision",
"showRevisionInIdHelpText": "Show revision as part of the node id.", "showRevisionInIdHelpText": "Show revision as part of the node id.",
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",
"enableHttpPostArgsHelpText": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.",
"disableHookSSLValidation": "Disable SSL Validation on Hooks",
"disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.",
"disabled": "Disabled", "disabled": "Disabled",
"disabledHelpText": "Enable or disable the Mercurial plugin.", "disabledHelpText": "Enable or disable the Mercurial plugin.",
"required": "This configuration value is required" "required": "This configuration value is required"

View File

@@ -37,7 +37,22 @@ from collections import defaultdict
from mercurial import cmdutil,util from mercurial import cmdutil,util
cmdtable = {} cmdtable = {}
command = cmdutil.command(cmdtable)
try:
from mercurial import registrar
command = registrar.command(cmdtable)
except (AttributeError, ImportError):
# Fallback to hg < 4.3 support
from mercurial import cmdutil
command = cmdutil.command(cmdtable)
try:
from mercurial.utils import dateutil
_parsedate = dateutil.parsedate
except ImportError:
# compat with hg < 4.6
from mercurial import util
_parsedate = util.parsedate
FILE_MARKER = '<files>' FILE_MARKER = '<files>'
@@ -201,7 +216,7 @@ class File_Printer:
description = 'n/a' description = 'n/a'
if not self.disableLastCommit: if not self.disableLastCommit:
linkrev = self.repo[file.linkrev()] linkrev = self.repo[file.linkrev()]
date = '%d %d' % util.parsedate(linkrev.date()) date = '%d %d' % _parsedate(linkrev.date())
description = linkrev.description() description = linkrev.description()
format = '%s %i %s %s\n' format = '%s %i %s %s\n'
if self.transport: if self.transport:

View File

@@ -36,7 +36,11 @@ from mercurial.hgweb import hgweb, wsgicgi
demandimport.enable() demandimport.enable()
u = uimod.ui() try:
u = uimod.ui.load()
except AttributeError:
# For installations earlier than Mercurial 4.1
u = uimod.ui()
u.setconfig('web', 'push_ssl', 'false') u.setconfig('web', 'push_ssl', 'false')
u.setconfig('web', 'allow_read', '*') u.setconfig('web', 'allow_read', '*')
@@ -45,7 +49,13 @@ u.setconfig('web', 'allow_push', '*')
u.setconfig('hooks', 'changegroup.scm', 'python:scmhooks.callback') u.setconfig('hooks', 'changegroup.scm', 'python:scmhooks.callback')
u.setconfig('hooks', 'pretxnchangegroup.scm', 'python:scmhooks.callback') u.setconfig('hooks', 'pretxnchangegroup.scm', 'python:scmhooks.callback')
# pass SCM_HTTP_POST_ARGS to enable experimental httppostargs protocol of mercurial
# SCM_HTTP_POST_ARGS is set by HgCGIServlet
# Issue 970: https://goo.gl/poascp
u.setconfig('experimental', 'httppostargs', os.environ['SCM_HTTP_POST_ARGS'])
# open repository
# SCM_REPOSITORY_PATH contains the repository path and is set by HgCGIServlet
r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH']) r = hg.repository(u, os.environ['SCM_REPOSITORY_PATH'])
application = hgweb(r) application = hgweb(r)
wsgicgi.launch(application) wsgicgi.launch(application)

View File

@@ -47,7 +47,7 @@ def printMessages(ui, msgs):
for line in msgs: for line in msgs:
if line.startswith("_e") or line.startswith("_n"): if line.startswith("_e") or line.startswith("_n"):
line = line[2:]; line = line[2:];
ui.warn(line); ui.warn('%s\n' % line.rstrip())
def callHookUrl(ui, repo, hooktype, node): def callHookUrl(ui, repo, hooktype, node):
abort = True abort = True
@@ -79,8 +79,10 @@ def callHookUrl(ui, repo, hooktype, node):
printMessages(ui, msg.splitlines(True)) printMessages(ui, msg.splitlines(True))
else: else:
ui.warn( "ERROR: scm-hook failed with an unknown error\n" ) ui.warn( "ERROR: scm-hook failed with an unknown error\n" )
ui.traceback()
except ValueError: except ValueError:
ui.warn( "scm-hook failed with an exception\n" ) ui.warn( "scm-hook failed with an exception\n" )
ui.traceback()
return abort return abort
def callback(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs): def callback(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):

View File

@@ -1,7 +1,8 @@
header = "%{pattern}" header = "%{pattern}"
changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{join(extras,',')}\n{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0" changeset = "{rev}:{node}{author}\n{date|hgdate}\n{branch}\n{parents}{extras}\n{tags}{file_adds}{file_mods}{file_dels}\n{desc}\0"
tag = "t {tag}\n" tag = "t {tag}\n"
file_add = "a {file_add}\n" file_add = "a {file_add}\n"
file_mod = "m {file_mod}\n" file_mod = "m {file_mod}\n"
file_del = "d {file_del}\n" file_del = "d {file_del}\n"
extra = "{key}={value|stringescape},"
footer = "%{pattern}" footer = "%{pattern}"

View File

@@ -30,6 +30,8 @@ public class HgConfigDtoToHgConfigMapperTest {
assertEquals("/etc/", config.getPythonPath()); assertEquals("/etc/", config.getPythonPath());
assertTrue(config.isShowRevisionInId()); assertTrue(config.isShowRevisionInId());
assertTrue(config.isUseOptimizedBytecode()); assertTrue(config.isUseOptimizedBytecode());
assertTrue(config.isDisableHookSSLValidation());
assertTrue(config.isEnableHttpPostArgs());
} }
private HgConfigDto createDefaultDto() { private HgConfigDto createDefaultDto() {
@@ -41,6 +43,8 @@ public class HgConfigDtoToHgConfigMapperTest {
configDto.setPythonPath("/etc/"); configDto.setPythonPath("/etc/");
configDto.setShowRevisionInId(true); configDto.setShowRevisionInId(true);
configDto.setUseOptimizedBytecode(true); configDto.setUseOptimizedBytecode(true);
configDto.setDisableHookSSLValidation(true);
configDto.setEnableHttpPostArgs(true);
return configDto; return configDto;
} }

View File

@@ -31,18 +31,29 @@
package sonia.scm.web; package sonia.scm.web;
import javax.servlet.http.HttpServletRequest; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryProvider;
import javax.servlet.http.HttpServletRequest;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.web.WireProtocolRequestMockFactory.CMDS_HEADS_KNOWN_NODES;
import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.BOOKMARKS;
import static sonia.scm.web.WireProtocolRequestMockFactory.Namespace.PHASES;
/** /**
* Unit tests for {@link HgPermissionFilter}. * Unit tests for {@link HgPermissionFilter}.
* *
@@ -60,9 +71,33 @@ public class HgPermissionFilterTest {
@Mock @Mock
private RepositoryProvider repositoryProvider; private RepositoryProvider repositoryProvider;
@Mock
private HgRepositoryHandler hgRepositoryHandler;
private WireProtocolRequestMockFactory wireProtocol = new WireProtocolRequestMockFactory("/scm/hg/repo");
@InjectMocks @InjectMocks
private HgPermissionFilter filter; private HgPermissionFilter filter;
@Before
public void setUp() {
when(hgRepositoryHandler.getConfig()).thenReturn(new HgConfig());
}
/**
* Tests {@link HgPermissionFilter#wrapRequestIfRequired(HttpServletRequest)}.
*/
@Test
public void testWrapRequestIfRequired() {
assertSame(request, filter.wrapRequestIfRequired(request));
HgConfig hgConfig = new HgConfig();
hgConfig.setEnableHttpPostArgs(true);
when(hgRepositoryHandler.getConfig()).thenReturn(hgConfig);
assertThat(filter.wrapRequestIfRequired(request), is(instanceOf(HgServletRequest.class)));
}
/** /**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}. * Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)}.
*/ */
@@ -81,8 +116,121 @@ public class HgPermissionFilterTest {
assertTrue(isWriteRequest("KA")); assertTrue(isWriteRequest("KA"));
} }
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with enabled httppostargs option.
*/
@Test
public void testIsWriteRequestWithEnabledHttpPostArgs() {
HgConfig config = new HgConfig();
config.setEnableHttpPostArgs(true);
when(hgRepositoryHandler.getConfig()).thenReturn(config);
assertFalse(isWriteRequest("POST"));
assertFalse(isWriteRequest("POST", "heads"));
assertTrue(isWriteRequest("POST", "unbundle"));
}
private boolean isWriteRequest(String method) { private boolean isWriteRequest(String method) {
return isWriteRequest(method, "capabilities");
}
private boolean isWriteRequest(String method, String command) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getQueryString()).thenReturn("cmd=" + command);
when(request.getMethod()).thenReturn(method); when(request.getMethod()).thenReturn(method);
return filter.isWriteRequest(request); return filter.isWriteRequest(request);
} }
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a
* fresh clone of a repository.
*/
@Test
public void testIsWriteRequestWithClone() {
assertIsReadRequest(wireProtocol.capabilities());
assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS));
assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
}
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a
* push of a single changeset.
*/
@Test
public void testIsWriteRequestWithSingleChangesetPush() {
assertIsReadRequest(wireProtocol.capabilities());
assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2")));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS));
assertIsWriteRequest(wireProtocol.unbundle(261L, "686173686564+6768033e216468247bd031a0a2d9876d79818f8f"));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsWriteRequest(wireProtocol.pushkey("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2&namespace=phases&new=0&old=1"));
}
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a
* push to a single changeset.
*/
@Test
public void testIsWriteRequestWithMultipleChangesetsPush() {
assertIsReadRequest(wireProtocol.capabilities());
assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98")));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS));
assertIsReadRequest(wireProtocol.branchmap());
assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS));
assertIsWriteRequest(wireProtocol.unbundle(746L, "686173686564+95373ca7cd5371cb6c49bb755ee451d9ec585845"));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsWriteRequest(wireProtocol.pushkey("ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1"));
}
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a
* push of multiple branches to a new repository.
*/
@Test
public void testIsWriteRequestWithMutlipleBranchesToNewRepositoryPush() {
assertIsReadRequest(wireProtocol.capabilities());
assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98")));
assertIsReadRequest(wireProtocol.known("c0ceccb3b2f0f5c977ff32b9337519e5f37942c2+187ddf37e237c370514487a0bb1a226f11a780b3+b5914611f84eae14543684b2721eec88b0edac12+8b63a323606f10c86b30465570c2574eb7a3a989"));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS));
assertIsWriteRequest(wireProtocol.unbundle(913L, "686173686564+6768033e216468247bd031a0a2d9876d79818f8f"));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsWriteRequest(wireProtocol.pushkey("ef5993bb4abb32a0565c347844c6d939fc4f4b98&namespace=phases&new=0&old=1"));
}
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a set of requests, which are used for a
* push of a bookmark.
*/
@Test
public void testIsWriteRequestWithBookmarkPush() {
assertIsReadRequest(wireProtocol.capabilities());
assertIsReadRequest(wireProtocol.batch(CMDS_HEADS_KNOWN_NODES.concat("ef5993bb4abb32a0565c347844c6d939fc4f4b98")));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsReadRequest(wireProtocol.listkeys(BOOKMARKS));
assertIsReadRequest(wireProtocol.listkeys(PHASES));
assertIsWriteRequest(wireProtocol.pushkey("markone&namespace=bookmarks&new=ef5993bb4abb32a0565c347844c6d939fc4f4b98&old="));
}
/**
* Tests {@link HgPermissionFilter#isWriteRequest(HttpServletRequest)} with a write request hidden in a batch GET
* request.
*
* @see <a href="https://goo.gl/poascp">Issue #970</a>
*/
@Test
public void testIsWriteRequestWithBookmarkPushInABatch() {
assertIsWriteRequest(wireProtocol.batch("pushkey key=markthree,namespace=bookmarks,new=187ddf37e237c370514487a0bb1a226f11a780b3,old="));
}
private void assertIsReadRequest(HttpServletRequest request) {
assertFalse(filter.isWriteRequest(request));
}
private void assertIsWriteRequest(HttpServletRequest request) {
assertTrue(filter.isWriteRequest(request));
}
} }

View File

@@ -0,0 +1,50 @@
package sonia.scm.web;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import org.junit.Test;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
public class HgServletInputStreamTest {
@Test
public void testReadAndCapture() throws IOException {
SampleServletInputStream original = new SampleServletInputStream("trillian.mcmillian@hitchhiker.com");
HgServletInputStream hgServletInputStream = new HgServletInputStream(original);
byte[] prefix = hgServletInputStream.readAndCapture(8);
assertEquals("trillian", new String(prefix, Charsets.US_ASCII));
byte[] wholeBytes = ByteStreams.toByteArray(hgServletInputStream);
assertEquals("trillian.mcmillian@hitchhiker.com", new String(wholeBytes, Charsets.US_ASCII));
}
@Test(expected = IllegalStateException.class)
public void testReadAndCaptureCalledTwice() throws IOException {
SampleServletInputStream original = new SampleServletInputStream("trillian.mcmillian@hitchhiker.com");
HgServletInputStream hgServletInputStream = new HgServletInputStream(original);
hgServletInputStream.readAndCapture(1);
hgServletInputStream.readAndCapture(1);
}
private static class SampleServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
private SampleServletInputStream(String data) {
input = new ByteArrayInputStream(data.getBytes());
}
@Override
public int read() {
return input.read();
}
}
}

View File

@@ -0,0 +1,114 @@
package sonia.scm.web;
import com.google.common.collect.Lists;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static org.mockito.Mockito.*;
public class WireProtocolRequestMockFactory {
public enum Namespace {
PHASES, BOOKMARKS;
}
public static final String CMDS_HEADS_KNOWN_NODES = "heads+%3Bknown+nodes%3D";
private String repositoryPath;
public WireProtocolRequestMockFactory(String repositoryPath) {
this.repositoryPath = repositoryPath;
}
public HttpServletRequest capabilities() {
return base("GET", "cmd=capabilities");
}
public HttpServletRequest listkeys(Namespace namespace) {
HttpServletRequest request = base("GET", "cmd=capabilities");
header(request, "vary", "X-HgArg-1");
header(request, "x-hgarg-1", namespaceValue(namespace));
return request;
}
public HttpServletRequest branchmap() {
return base("GET", "cmd=branchmap");
}
public HttpServletRequest batch(String... args) {
HttpServletRequest request = base("GET", "cmd=batch");
args(request, "cmds", args);
return request;
}
public HttpServletRequest unbundle(long contentLength, String... heads) {
HttpServletRequest request = base("POST", "cmd=unbundle");
header(request, "Content-Length", String.valueOf(contentLength));
args(request, "heads", heads);
return request;
}
public HttpServletRequest pushkey(String... keys) {
HttpServletRequest request = base("POST", "cmd=pushkey");
args(request, "key", keys);
return request;
}
public HttpServletRequest known(String... nodes) {
HttpServletRequest request = base("GET", "cmd=known");
args(request, "nodes", nodes);
return request;
}
private void args(HttpServletRequest request, String prefix, String[] values) {
List<String> headers = Lists.newArrayList();
StringBuilder vary = new StringBuilder();
for ( int i=0; i<values.length; i++ ) {
String header = "X-HgArg-" + (i+1);
if (i>0) {
vary.append(",");
}
vary.append(header);
headers.add(header);
header(request, header, prefix + "=" + values[i]);
}
header(request, "Vary", vary.toString());
when(request.getHeaderNames()).thenReturn(Collections.enumeration(headers));
}
private HttpServletRequest base(String method, String queryStringValue) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getRequestURI()).thenReturn(repositoryPath);
when(request.getMethod()).thenReturn(method);
queryString(request, queryStringValue);
header(request, "Accept", "application/mercurial-0.1");
header(request, "Accept-Encoding", "identity");
header(request, "User-Agent", "mercurial/proto-1.0 (Mercurial 4.3.1)");
return request;
}
private void queryString(HttpServletRequest request, String queryString) {
when(request.getQueryString()).thenReturn(queryString);
}
private void header(HttpServletRequest request, String header, String value) {
when(request.getHeader(header)).thenReturn(value);
}
private String namespaceValue(Namespace namespace) {
return "namespace=" + namespace.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,192 @@
/**
* Copyright (c) 2018, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link WireProtocol}.
*/
@RunWith(MockitoJUnitRunner.class)
public class WireProtocolTest {
@Mock
private HttpServletRequest request;
@Test
public void testIsWriteRequestOnPost() {
assertIsWriteRequest("capabilities", "unbundle");
}
@Test
public void testIsWriteRequest() {
assertIsWriteRequest("unbundle");
assertIsWriteRequest("capabilities", "unbundle");
assertIsWriteRequest("capabilities", "postkeys");
assertIsReadRequest();
assertIsReadRequest("capabilities");
assertIsReadRequest("capabilities", "branches", "branchmap");
}
private void assertIsWriteRequest(String... commands) {
List<String> cmdList = Lists.newArrayList(commands);
assertTrue(WireProtocol.isWriteRequest(cmdList));
}
private void assertIsReadRequest(String... commands) {
List<String> cmdList = Lists.newArrayList(commands);
assertFalse(WireProtocol.isWriteRequest(cmdList));
}
@Test
public void testGetCommandsOf() {
expectQueryCommand("capabilities", "cmd=capabilities");
expectQueryCommand("unbundle", "cmd=unbundle");
expectQueryCommand("unbundle", "prefix=stuff&cmd=unbundle");
expectQueryCommand("unbundle", "cmd=unbundle&suffix=stuff");
expectQueryCommand("unbundle", "prefix=stuff&cmd=unbundle&suffix=stuff");
expectQueryCommand("unbundle", "bool=&cmd=unbundle");
expectQueryCommand("unbundle", "bool&cmd=unbundle");
expectQueryCommand("unbundle", "prefix=stu==ff&cmd=unbundle");
}
@Test
public void testGetCommandsOfWithHgArgsPost() throws IOException {
when(request.getMethod()).thenReturn("POST");
when(request.getQueryString()).thenReturn("cmd=batch");
when(request.getIntHeader("X-HgArgs-Post")).thenReturn(29);
when(request.getHeaderNames()).thenReturn(Collections.enumeration(Lists.newArrayList("X-HgArgs-Post")));
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("cmds=lheads+%3Bknown+nodes%3D"));
List<String> commands = WireProtocol.commandsOf(new HgServletRequest(request));
assertThat(commands, contains("batch", "lheads", "known"));
}
@Test
public void testGetCommandsOfWithBatch() {
prepareBatch("cmds=heads ;known nodes,ef5993bb4abb32a0565c347844c6d939fc4f4b98");
List<String> commands = WireProtocol.commandsOf(request);
assertThat(commands, contains("batch", "heads", "known"));
}
@Test
public void testGetCommandsOfWithBatchEncoded() {
prepareBatch("cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98");
List<String> commands = WireProtocol.commandsOf(request);
assertThat(commands, contains("batch", "heads", "known"));
}
@Test
public void testGetCommandsOfWithBatchAndMutlipleLines() {
prepareBatch(
"cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98",
"cmds=unbundle; postkeys",
"cmds= branchmap p1=r2,p2=r4; listkeys"
);
List<String> commands = WireProtocol.commandsOf(request);
assertThat(commands, contains("batch", "heads", "known", "unbundle", "postkeys", "branchmap", "listkeys"));
}
private void prepareBatch(String... args) {
when(request.getQueryString()).thenReturn("cmd=batch");
List<String> headers = Lists.newArrayList();
for (int i=0; i<args.length; i++) {
String header = "X-HgArg-" + (i+1);
headers.add(header);
when(request.getHeader(header)).thenReturn(args[i]);
}
when(request.getHeaderNames()).thenReturn(Collections.enumeration(headers));
}
@Test(expected = IllegalArgumentException.class)
public void testGetCommandsOfWithMultipleCommandsInQueryString() {
when(request.getQueryString()).thenReturn("cmd=abc&cmd=def");
WireProtocol.commandsOf(request);
}
@Test
public void testGetCommandsOfWithoutCmdInQueryString() {
when(request.getQueryString()).thenReturn("abc=def&123=456");
assertTrue(WireProtocol.commandsOf(request).isEmpty());
}
@Test
public void testGetCommandsOfWithEmptyQueryString() {
when(request.getQueryString()).thenReturn("");
assertTrue(WireProtocol.commandsOf(request).isEmpty());
}
@Test
public void testGetCommandsOfWithNullQueryString() {
assertTrue(WireProtocol.commandsOf(request).isEmpty());
}
private void expectQueryCommand(String expected, String queryString) {
when(request.getQueryString()).thenReturn(queryString);
List<String> commands = WireProtocol.commandsOf(request);
assertEquals(1, commands.size());
assertTrue(commands.contains(expected));
}
private static class BufferedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
BufferedServletInputStream(String content) {
this.input = new ByteArrayInputStream(content.getBytes(Charsets.US_ASCII));
}
@Override
public int read() {
return input.read();
}
}
}

View File

@@ -21,23 +21,28 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
super(context, repository); super(context, repository);
} }
@Override @Override
@SuppressWarnings("unchecked") public Modifications getModifications(String revisionOrTransactionId) {
public Modifications getModifications(String revision) { Modifications modifications;
final Modifications modifications = new Modifications();
log.debug("get modifications {}", revision);
try { try {
if (SvnUtil.isTransactionEntryId(revision)) { if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) {
modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId));
SVNLookClient client = SVNClientManager.newInstance().getLookClient();
client.doGetChanged(context.getDirectory(), SvnUtil.getTransactionId(revision),
e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true);
return modifications;
} else { } else {
modifications = getModificationFromRevision(revisionOrTransactionId);
}
return modifications;
} catch (SVNException ex) {
throw new InternalRepositoryException(
repository,
"failed to get svn modifications for " + revisionOrTransactionId,
ex
);
}
}
@SuppressWarnings("unchecked")
private Modifications getModificationFromRevision(String revision) throws SVNException {
log.debug("get svn modifications from revision: {}", revision);
long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); long revisionNumber = SvnUtil.getRevisionNumber(revision, repository);
SVNRepository repo = open(); SVNRepository repo = open();
Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber, Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber,
@@ -45,17 +50,22 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
if (Util.isNotEmpty(entries)) { if (Util.isNotEmpty(entries)) {
return SvnUtil.createModifications(entries.iterator().next(), revision); return SvnUtil.createModifications(entries.iterator().next(), revision);
} }
}
} catch (SVNException ex) {
throw new InternalRepositoryException(repository, "could not open repository", ex);
}
return null; return null;
} }
private Modifications getModificationsFromTransaction(String transaction) throws SVNException {
log.debug("get svn modifications from transaction: {}", transaction);
final Modifications modifications = new Modifications();
SVNLookClient client = SVNClientManager.newInstance().getLookClient();
client.doGetChanged(context.getDirectory(), transaction,
e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true);
return modifications;
}
@Override @Override
public Modifications getModifications(ModificationsCommandRequest request) { public Modifications getModifications(ModificationsCommandRequest request) {
return getModifications(request.getRevision()); return getModifications(request.getRevision());
} }
} }

View File

@@ -1,7 +1,42 @@
{ {
"scm-svn-plugin": { "scm-svn-plugin": {
"information": { "information": {
"checkout" : "Repository auschecken" "checkout": "Repository auschecken"
},
"config": {
"link": "Subversion",
"title": "Subversion Konfiguration",
"compatibility": "Version Kompatibilität",
"compatibilityHelpText": "Gibt an, mit welcher Subversion Version die Repositories kompatibel sind.",
"compatibility-values": {
"none": "Keine Kompatibilität",
"pre14": "Vor 1.4 kompatibel",
"pre15": "Vor 1.5 kompatibel",
"pre16": "Vor 1.6 kompatibel",
"pre17": "Vor 1.7 kompatibel",
"with17": "Mit 1.7 kompatibel"
},
"enabledGZip": "GZip Compression aktivieren",
"enabledGZipHelpText": "Aktiviert GZip Kompression für SVN Responses",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin",
"required": "Dieser Konfigurationswert wird benötigt"
}
},
"permissions": {
"configuration": {
"read": {
"svn": {
"displayName": "Subversion Konfiguration lesen",
"description": "Darf die Subversion Konfiguration lesen"
}
},
"write": {
"svn": {
"displayName": "Subversion Konfiguration schreiben",
"description": "Darf die Subversion Konfiguration verändern"
}
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@
"link": "Subversion", "link": "Subversion",
"title": "Subversion Configuration", "title": "Subversion Configuration",
"compatibility": "Version Compatibility", "compatibility": "Version Compatibility",
"compatibilityHelpText": "Specifies with which subversion version repositories are compatible.", "compatibilityHelpText": "Specifies with which Subversion version repositories are compatible.",
"compatibility-values": { "compatibility-values": {
"none": "No compatibility", "none": "No compatibility",
"pre14": "Pre 1.4 Compatible", "pre14": "Pre 1.4 Compatible",
@@ -17,9 +17,9 @@
"with17": "With 1.7 Compatible" "with17": "With 1.7 Compatible"
}, },
"enabledGZip": "Enable GZip Compression", "enabledGZip": "Enable GZip Compression",
"enabledGZipHelpText": "Enable GZip compression for svn responses.", "enabledGZipHelpText": "Enable GZip compression for SVN responses.",
"disabled": "Disabled", "disabled": "Disabled",
"disabledHelpText": "Enable or disable the Git plugin", "disabledHelpText": "Enable or disable the SVN plugin",
"required": "This configuration value is required" "required": "This configuration value is required"
} }
}, },

View File

@@ -64,7 +64,6 @@
<assembleDirectory>${exploded.directory}</assembleDirectory> <assembleDirectory>${exploded.directory}</assembleDirectory>
<repoPath>lib</repoPath> <repoPath>lib</repoPath>
<repositoryLayout>flat</repositoryLayout> <repositoryLayout>flat</repositoryLayout>
<includeConfigurationDirectoryInClasspath>true</includeConfigurationDirectoryInClasspath>
<daemons> <daemons>
<daemon> <daemon>
@@ -306,8 +305,8 @@
</profiles> </profiles>
<properties> <properties>
<commons.daemon.version>1.0.15</commons.daemon.version> <commons.daemon.version>1.1.0</commons.daemon.version>
<commons.daemon.native.version>1.0.15.1</commons.daemon.native.version> <commons.daemon.native.version>1.1.0</commons.daemon.native.version>
<exploded.directory>${project.build.directory}/appassembler/commons-daemon/scm-server</exploded.directory> <exploded.directory>${project.build.directory}/appassembler/commons-daemon/scm-server</exploded.directory>
</properties> </properties>

View File

@@ -1,7 +1,7 @@
// @flow // @flow
import React from "react"; import React from "react";
import type {Branch} from "@scm-manager/ui-types"; import type { Branch } from "@scm-manager/ui-types";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import classNames from "classnames"; import classNames from "classnames";
import DropDown from "./forms/DropDown"; import DropDown from "./forms/DropDown";
@@ -39,7 +39,9 @@ class BranchSelector extends React.Component<Props, State> {
} }
componentDidMount() { componentDidMount() {
const selectedBranch = this.props.branches.find(branch => branch.name === this.props.selectedBranch); const selectedBranch = this.props.branches.find(
branch => branch.name === this.props.selectedBranch
);
this.setState({ selectedBranch }); this.setState({ selectedBranch });
} }

View File

@@ -0,0 +1,44 @@
// @flow
import React from "react";
import Button from "./Button";
type Props = {
firstlabel: string,
secondlabel: string,
firstAction?: (event: Event) => void,
secondAction?: (event: Event) => void,
firstIsSelected: boolean
};
class ButtonGroup extends React.Component<Props> {
render() {
const { firstlabel, secondlabel, firstAction, secondAction, firstIsSelected } = this.props;
let showFirstColor = "";
let showSecondColor = "";
if (firstIsSelected) {
showFirstColor += "link is-selected";
} else {
showSecondColor += "link is-selected";
}
return (
<div className="buttons has-addons">
<Button
label={firstlabel}
color={showFirstColor}
action={firstAction}
/>
<Button
label={secondlabel}
color={showSecondColor}
action={secondAction}
/>
</div>
);
}
}
export default ButtonGroup;

View File

@@ -1,7 +1,8 @@
//@flow //@flow
import React from "react"; import React from "react";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import SubmitButton, { type ButtonProps } from "./SubmitButton"; import { type ButtonProps } from "./Button";
import SubmitButton from "./SubmitButton";
import classNames from "classnames"; import classNames from "classnames";
const styles = { const styles = {

View File

@@ -5,6 +5,9 @@ export { default as Button } from "./Button.js";
export { default as CreateButton } from "./CreateButton.js"; export { default as CreateButton } from "./CreateButton.js";
export { default as DeleteButton } from "./DeleteButton.js"; export { default as DeleteButton } from "./DeleteButton.js";
export { default as EditButton } from "./EditButton.js"; export { default as EditButton } from "./EditButton.js";
export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton.js";
export { default as SubmitButton } from "./SubmitButton.js"; export { default as SubmitButton } from "./SubmitButton.js";
export {default as DownloadButton} from "./DownloadButton.js"; export { default as DownloadButton } from "./DownloadButton.js";
export { default as ButtonGroup } from "./ButtonGroup.js";
export {
default as RemoveEntryOfTableButton
} from "./RemoveEntryOfTableButton.js";

View File

@@ -36,9 +36,9 @@ class ConfigurationBinder {
binder.bind("config.navigation", ConfigNavLink, configPredicate); binder.bind("config.navigation", ConfigNavLink, configPredicate);
// route for global configuration, passes the link from the index resource to component // route for global configuration, passes the link from the index resource to component
const ConfigRoute = ({ url, links }) => { const ConfigRoute = ({ url, links, ...additionalProps }) => {
const link = links[linkName].href; const link = links[linkName].href;
return this.route(url + to, <ConfigurationComponent link={link}/>); return this.route(url + to, <ConfigurationComponent link={link} {...additionalProps} />);
}; };
// bind config route to extension point // bind config route to extension point
@@ -63,9 +63,9 @@ class ConfigurationBinder {
// route for global configuration, passes the current repository to component // route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository}) => { const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href const link = repository._links[linkName].href;
return this.route(url + to, <RepositoryComponent repository={repository} link={link}/>); return this.route(url + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
}; };
// bind config route to extension point // bind config route to extension point

View File

@@ -48,7 +48,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
<AddButton <AddButton
label={buttonLabel} label={buttonLabel}
action={this.addButtonClicked} action={this.addButtonClicked}
disabled={disabled} disabled={disabled || this.state.entryToAdd ===""}
/> />
</div> </div>
); );

View File

@@ -54,7 +54,7 @@ class Select extends React.Component<Props> {
> >
{options.map(opt => { {options.map(opt => {
return ( return (
<option value={opt.value} key={opt.value}> <option value={opt.value} key={"KEY_" + opt.value}>
{opt.label} {opt.label}
</option> </option>
); );

View File

@@ -0,0 +1,11 @@
// @flow
export type RepositoryRole = {
name: string,
verbs: string[]
};
export type AvailableRepositoryPermissions = {
availableVerbs: string[],
availableRoles: RepositoryRole[]
};

View File

@@ -7,7 +7,7 @@ export type Permission = PermissionCreateEntry & {
export type PermissionCreateEntry = { export type PermissionCreateEntry = {
name: string, name: string,
type: string, verbs: string[],
groupPermission: boolean groupPermission: boolean
} }

View File

@@ -24,3 +24,5 @@ export type { Permission, PermissionCreateEntry, PermissionCollection } from "./
export type { SubRepository, File } from "./Sources"; export type { SubRepository, File } from "./Sources";
export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { SelectValue, AutocompleteObject } from "./Autocomplete";
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";

View File

@@ -0,0 +1,72 @@
{
"login": {
"title": "Anmeldung",
"subtitle": "Bitte anmelden, um fortzufahren.",
"logo-alt": "SCM-Manager",
"username-placeholder": "Benutzername",
"password-placeholder": "Passwort",
"submit": "Anmelden"
},
"logout": {
"error": {
"title": "Abmeldung fehlgeschlagen",
"subtitle": "Während der Abmeldung ist ein Fehler aufgetreten."
}
},
"app": {
"error": {
"title": "Fehler",
"subtitle": "Ein unbekannter Fehler ist aufgetreten."
}
},
"error-notification": {
"prefix": "Fehler",
"loginLink": "Erneute Anmeldung",
"timeout": "Die Session ist abgelaufen.",
"wrong-login-credentials": "Ungültige Anmeldedaten"
},
"loading": {
"alt": "Lade ..."
},
"logo": {
"alt": "SCM-Manager"
},
"primary-navigation": {
"repositories": "Repositories",
"users": "Benutzer",
"logout": "Abmelden",
"groups": "Gruppen",
"config": "Einstellungen"
},
"paginator": {
"next": "Weiter",
"previous": "Zurück"
},
"profile": {
"navigation-label": "Navigation",
"actions-label": "Aktionen",
"username": "Benutzername",
"displayName": "Anzeigename",
"mail": "E-Mail",
"groups": "Gruppen",
"information": "Informationen",
"change-password": "Passwort ändern",
"error-title": "Fehler",
"error-subtitle": "Das Profil kann nicht angezeigt werden.",
"error": "Fehler",
"error-message": "'me' ist nicht definiert"
},
"password": {
"label": "Passwort",
"newPassword": "Neues Passwort",
"passwordHelpText": "Klartext Passwort des Benutzers.",
"passwordConfirmHelpText": "Passwort zur Bestätigen wiederholen.",
"currentPassword": "Aktuelles Passwort",
"currentPasswordHelpText": "Dieses Passwort wird momentan bereits verwendet.",
"confirmPassword": "Passwort wiederholen",
"passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein!",
"passwordConfirmFailed": "Passwörter müssen identisch sein!",
"submit": "Speichern",
"changedSuccessfully": "Passwort erfolgreich geändert!"
}
}

View File

@@ -0,0 +1,93 @@
{
"config": {
"navigation-title": "Navigation"
},
"global-config": {
"title": "Einstellungen",
"navigation-label": "Globale Einstellungen",
"error-title": "Fehler",
"error-subtitle": "Unbekannter Einstellungen Fehler"
},
"config-form": {
"submit": "Speichern",
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
"no-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Bearbeiten der Einstellungen!"
},
"proxy-settings": {
"name": "Proxy Einstellungen",
"proxy-password": "Proxy Passwort",
"proxy-port": "Proxy Port",
"proxy-server": "Proxy Server",
"proxy-user": "Proxy Benutzer",
"enable-proxy": "Proxy aktivieren",
"proxy-excludes": "Proxy Excludes",
"remove-proxy-exclude-button": "Proxy Exclude löschen",
"add-proxy-exclude-error": "Der Proxy Exclude ist ungültig",
"add-proxy-exclude-textfield": "Neue Proxy Excludes hinzufügen",
"add-proxy-exclude-button": "Proxy Exclude hinzufügen"
},
"base-url-settings": {
"name": "Base URL Einstellungen",
"base-url": "Base URL",
"force-base-url": "Base URL erzwingen"
},
"admin-settings": {
"name": "Administrations Einstellungen",
"admin-groups": "Admin Gruppen",
"admin-users": "Admin Benutzer",
"remove-group-button": "Admin Group löschen",
"remove-user-button": "Admin Benutzer löschen",
"add-group-error": "Der eingegebene Gruppenname ist ungültig",
"add-group-textfield": "Neue Gruppe mit Administrationsrechten hinzufügen",
"add-group-button": "Admin Gruppe hinzufügen",
"add-user-error": "Der eingegebene Benutzername ist ungültig",
"add-user-textfield": "Neuen Benutzer mit Administrationsrechten hinzufügen",
"add-user-button": "Admin Benutzer hinzufügen"
},
"login-attempt": {
"name": "Anmeldeversuche",
"login-attempt-limit": "Limit für Anmeldeversuche",
"login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuchen"
},
"general-settings": {
"realm-description": "Realm Beschreibung",
"enable-repository-archive": "Repository Archiv aktivieren",
"disable-grouping-grid": "Gruppen deaktivieren",
"date-format": "Datumsformat",
"anonymous-access-enabled": "Anonyme Zugriffe erlauben",
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
"plugin-url": "Plugin URL",
"enabled-xsrf-protection": "XSRF Protection aktivieren",
"default-namespace-strategy": "Default Namespace Strategie"
},
"validation": {
"date-format-invalid": "Das Datumsformat ist ungültig",
"login-attempt-limit-timeout-invalid": "Dies ist keine Zahl",
"login-attempt-limit-invalid": "Dies ist keine Zahl",
"plugin-url-invalid": "Dies ist keine gültige URL"
},
"help": {
"realmDescriptionHelpText": "Beschreibung des Authentication Realm.",
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.",
"pluginRepositoryHelpText": "Die URL des Plugin Repositories. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur",
"enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.",
"enableRepositoryArchiveHelpText": "Repository Archive aktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf öffentliche Repositories.",
"skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.",
"adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.",
"adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.",
"forceBaseUrlHelpText": "Zugriffe, die von einer anderen URL kommen, werden auf die Base URL weiter geleitet.",
"baseUrlHelpText": "Die URL der Applikation mit Kontextpfad, z.B. http://localhost:8080/scm",
"loginAttemptLimitHelpText": "Maximale Anzahl von Anmeldeversuchen. Durch Verwendung von -1 wird die Begrenzung der Anmeldeversuche deaktiviert.",
"loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche, deaktiviert wurden.",
"enableProxyHelpText": "Proxy aktivieren",
"proxyPortHelpText": "Der Proxy Port",
"proxyPasswordHelpText": "Das Passwort für die Proxy Server Anmeldung.",
"proxyServerHelpText": "Der Proxy Server",
"proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.",
"proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.",
"enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.",
"defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces."
}
}

Some files were not shown because too many files have changed in this diff Show More