mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
Merge with 2.0.0-m3
This commit is contained in:
76
docs/mercurial/clone-empty.md
Normal file
76
docs/mercurial/clone-empty.md
Normal 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
|
||||
```
|
||||
117
docs/mercurial/push-bookmark.md
Normal file
117
docs/mercurial/push-bookmark.md
Normal 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
|
||||
```
|
||||
167
docs/mercurial/push-multiple-branches-to-new.md
Normal file
167
docs/mercurial/push-multiple-branches-to-new.md
Normal 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
|
||||
```
|
||||
183
docs/mercurial/push-multiple-branches.md
Normal file
183
docs/mercurial/push-multiple-branches.md
Normal 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
|
||||
```
|
||||
147
docs/mercurial/push-single-changeset.md
Normal file
147
docs/mercurial/push-single-changeset.md
Normal 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
161
pom.xml
@@ -86,20 +86,6 @@
|
||||
<url>http://maven.scm-manager.org/nexus/content/groups/public</url>
|
||||
</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>
|
||||
|
||||
<pluginRepositories>
|
||||
@@ -174,18 +160,6 @@
|
||||
<artifactId>assertj-core</artifactId>
|
||||
</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>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -376,6 +350,70 @@
|
||||
<version>3.10.0</version>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -418,10 +456,11 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.0</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<version>3.0.0-M1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce-java</id>
|
||||
@@ -443,11 +482,31 @@
|
||||
<requireMavenVersion>
|
||||
<version>[3.1,)</version>
|
||||
</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>
|
||||
<fail>true</fail>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>extra-enforcer-rules</artifactId>
|
||||
<version>1.0-beta-7</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
@@ -636,9 +695,15 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-site-plugin</artifactId>
|
||||
<version>3.2</version>
|
||||
<configuration>
|
||||
<reportPlugins>
|
||||
<version>3.7</version>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
@@ -669,19 +734,13 @@
|
||||
<artifactId>maven-pmd-plugin</artifactId>
|
||||
<version>2.7.1</version>
|
||||
<configuration>
|
||||
<linkXref>true</linkXref>
|
||||
<sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
|
||||
<targetJdk>${project.build.javaLevel}</targetJdk>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</reportPlugins>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
</reporting>
|
||||
|
||||
<profiles>
|
||||
|
||||
@@ -726,17 +785,9 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<doclet>org.jboss.apiviz.APIviz</doclet>
|
||||
<docletArtifact>
|
||||
<groupId>org.jboss.apiviz</groupId>
|
||||
<artifactId>apiviz</artifactId>
|
||||
<version>1.3.2.GA</version>
|
||||
</docletArtifact>
|
||||
<additionalparam>
|
||||
-sourceclasspath ${project.build.outputDirectory}
|
||||
-nopackagediagram
|
||||
</additionalparam>
|
||||
<failOnError>false</failOnError>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -770,8 +821,8 @@
|
||||
<junit.version>5.2.0</junit.version>
|
||||
|
||||
<!-- logging libraries -->
|
||||
<slf4j.version>1.7.22</slf4j.version>
|
||||
<logback.version>1.1.10</logback.version>
|
||||
<slf4j.version>1.7.25</slf4j.version>
|
||||
<logback.version>1.2.3</logback.version>
|
||||
<servlet.version>3.0.1</servlet.version>
|
||||
|
||||
<jaxrs.version>2.0.1</jaxrs.version>
|
||||
@@ -780,21 +831,22 @@
|
||||
<enunciate.version>2.11.1</enunciate.version>
|
||||
<jackson.version>2.8.6</jackson.version>
|
||||
<guice.version>4.0</guice.version>
|
||||
<jaxb.version>2.3.0</jaxb.version>
|
||||
|
||||
<!-- event bus -->
|
||||
<legman.version>1.4.2</legman.version>
|
||||
|
||||
<!-- webserver -->
|
||||
<jetty.version>9.2.10.v20150310</jetty.version>
|
||||
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
|
||||
<jetty.version>9.4.14.v20181114</jetty.version>
|
||||
<jetty.maven.version>9.4.14.v20181114</jetty.maven.version>
|
||||
|
||||
<!-- security libraries -->
|
||||
<ssp.version>1.1.0</ssp.version>
|
||||
<shiro.version>1.4.0</shiro.version>
|
||||
|
||||
<!-- repostitory libraries -->
|
||||
<jgit.version>v4.5.2.201704071617-r-scm1</jgit.version>
|
||||
<svnkit.version>1.8.15-scm1</svnkit.version>
|
||||
<!-- repository libraries -->
|
||||
<jgit.version>v4.5.3.201708160445-r-scm1</jgit.version>
|
||||
<svnkit.version>1.9.0-scm3</svnkit.version>
|
||||
|
||||
<!-- util libraries -->
|
||||
<guava.version>26.0-jre</guava.version>
|
||||
@@ -806,6 +858,7 @@
|
||||
|
||||
<!-- build properties -->
|
||||
<project.build.javaLevel>1.8</project.build.javaLevel>
|
||||
<project.test.javaLevel>1.8</project.test.javaLevel>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<netbeans.hint.license>SCM-BSD</netbeans.hint.license>
|
||||
<jdk.classifier />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -29,71 +29,32 @@
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
package sonia.scm.cli.config;
|
||||
|
||||
/**
|
||||
* Type of permissionPrefix for a {@link Repository}.
|
||||
* SecretKeyStore is able to read and write secret keys.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.60
|
||||
*/
|
||||
public enum PermissionType
|
||||
{
|
||||
|
||||
/** read permision */
|
||||
READ(0, "repository:read,pull:"),
|
||||
|
||||
/** read and write permissionPrefix */
|
||||
WRITE(10, "repository:read,pull,push:"),
|
||||
public interface SecretKeyStore {
|
||||
|
||||
/**
|
||||
* read, write and
|
||||
* also the ability to manage the properties and permissions
|
||||
* Writes the given secret key to the store.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
* @return secret key or {@code null}
|
||||
*/
|
||||
private PermissionType(int value, String permissionPrefix)
|
||||
{
|
||||
this.value = value;
|
||||
this.permissionPrefix = permissionPrefix;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
String get();
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @since 2.0.0
|
||||
* Removes the secret key from store.
|
||||
*/
|
||||
public String getPermissionPrefix()
|
||||
{
|
||||
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;
|
||||
void remove();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,58 +30,31 @@
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
package sonia.scm.cli.config;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import sonia.scm.repository.PermissionType;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class RepositoryPermissionTest
|
||||
{
|
||||
public class EncryptionSecretKeyStoreWrapperTest {
|
||||
|
||||
private SecretKeyStore secretKeyStore = new InMemorySecretKeyStore();
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testImplies()
|
||||
{
|
||||
RepositoryPermission p = new RepositoryPermission("asd",
|
||||
PermissionType.READ);
|
||||
public void testEncryptionKeyStoreWrapper() {
|
||||
EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore);
|
||||
wrapper.set("mysecretkey");
|
||||
|
||||
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
|
||||
assertFalse(p.implies(new RepositoryPermission("asd",
|
||||
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)));
|
||||
assertEquals("mysecretkey", wrapper.get());
|
||||
assertTrue(secretKeyStore.get().startsWith(EncryptionSecretKeyStoreWrapper.ENCRYPTED_PREFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testImpliesWithWildcard()
|
||||
{
|
||||
RepositoryPermission p = new RepositoryPermission("*",
|
||||
PermissionType.OWNER);
|
||||
|
||||
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
|
||||
assertTrue(p.implies(new RepositoryPermission("bdb",
|
||||
PermissionType.OWNER)));
|
||||
assertTrue(p.implies(new RepositoryPermission("cgd",
|
||||
PermissionType.WRITE)));
|
||||
public void testEncryptionKeyStoreWrapperWithOldUnencryptedKey() {
|
||||
secretKeyStore.set("mysecretkey");
|
||||
EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore);
|
||||
assertEquals("mysecretkey", wrapper.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -47,6 +47,7 @@
|
||||
<groupId>org.slf4j</groupId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@@ -143,6 +144,28 @@
|
||||
<version>${legman.version}</version>
|
||||
</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 -->
|
||||
|
||||
<dependency>
|
||||
@@ -193,6 +216,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<useStandardDocletOptions>true</useStandardDocletOptions>
|
||||
<charset>${project.build.sourceEncoding}</charset>
|
||||
@@ -216,6 +240,20 @@
|
||||
<link>http://www.slf4j.org/api/</link>
|
||||
<link>http://shiro.apache.org/static/${shiro.version}/apidocs/</link>
|
||||
</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>
|
||||
</plugin>
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ import java.util.Set;
|
||||
@XmlRootElement(name = "repositories")
|
||||
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
|
||||
|
||||
|
||||
private static final long serialVersionUID = 3486560714961909711L;
|
||||
|
||||
private String contact;
|
||||
@@ -81,6 +80,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
private Long lastModified;
|
||||
private String namespace;
|
||||
private String name;
|
||||
@XmlElement(name = "permission")
|
||||
private final Set<RepositoryPermission> permissions = new HashSet<>();
|
||||
@XmlElement(name = "public")
|
||||
private boolean publicReadable = false;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import groovy.lang.Singleton;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -18,7 +17,6 @@ import java.nio.file.Path;
|
||||
* @author Mohamed Karray
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Singleton
|
||||
public class RepositoryLocationResolver {
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
@@ -37,12 +37,19 @@ package sonia.scm.repository;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import sonia.scm.security.PermissionObject;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
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 ------------------------------------------------------------
|
||||
|
||||
@@ -60,54 +67,19 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
|
||||
private boolean groupPermission = false;
|
||||
private String name;
|
||||
private PermissionType type = PermissionType.READ;
|
||||
@XmlElement(name = "verb")
|
||||
private Collection<String> verbs;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryPermission}.
|
||||
* This constructor is used by JAXB.
|
||||
*
|
||||
* This constructor is used by JAXB and mapstruct.
|
||||
*/
|
||||
public RepositoryPermission() {}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ}
|
||||
* for the specified user.
|
||||
*
|
||||
*
|
||||
* @param name name of the user
|
||||
*/
|
||||
public RepositoryPermission(String name)
|
||||
public RepositoryPermission(String name, Collection<String> verbs, boolean groupPermission)
|
||||
{
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs));
|
||||
this.groupPermission = groupPermission;
|
||||
}
|
||||
|
||||
@@ -137,7 +109,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
final RepositoryPermission other = (RepositoryPermission) obj;
|
||||
|
||||
return Objects.equal(name, other.name)
|
||||
&& Objects.equal(type, other.type)
|
||||
&& CollectionUtils.isEqualCollection(verbs, other.verbs)
|
||||
&& Objects.equal(groupPermission, other.groupPermission);
|
||||
}
|
||||
|
||||
@@ -150,7 +122,9 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
@Override
|
||||
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-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("type", type)
|
||||
.add("verbs", verbs)
|
||||
.add("groupPermission", groupPermission)
|
||||
.toString();
|
||||
//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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,11 @@ import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.IncomingCommand;
|
||||
import sonia.scm.repository.spi.IncomingCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -94,8 +93,7 @@ public final class IncomingCommandBuilder
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.checkPermission(new RepositoryPermission(remoteRepository,
|
||||
PermissionType.READ));
|
||||
subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
|
||||
|
||||
request.setRemoteRepository(remoteRepository);
|
||||
|
||||
|
||||
@@ -34,12 +34,11 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.OutgoingCommand;
|
||||
import sonia.scm.repository.spi.OutgoingCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -84,8 +83,7 @@ public final class OutgoingCommandBuilder
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.checkPermission(new RepositoryPermission(remoteRepository,
|
||||
PermissionType.READ));
|
||||
subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
|
||||
|
||||
request.setRemoteRepository(remoteRepository);
|
||||
|
||||
|
||||
@@ -38,11 +38,10 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.PullCommand;
|
||||
import sonia.scm.repository.spi.PullCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -96,9 +95,7 @@ public final class PullCommandBuilder
|
||||
public PullResponse pull(String url) throws IOException {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
//J-
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(localRepository, PermissionType.WRITE)
|
||||
);
|
||||
subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
URL remoteUrl = new URL(url);
|
||||
@@ -124,12 +121,8 @@ public final class PullCommandBuilder
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
//J-
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(localRepository, PermissionType.WRITE)
|
||||
);
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(remoteRepository, PermissionType.READ)
|
||||
);
|
||||
subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
|
||||
subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
request.reset();
|
||||
|
||||
@@ -39,11 +39,10 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.PushCommand;
|
||||
import sonia.scm.repository.spi.PushCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -92,9 +91,7 @@ public final class PushCommandBuilder
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
//J-
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(remoteRepository, PermissionType.WRITE)
|
||||
);
|
||||
subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
logger.info("push changes to repository {}", remoteRepository.getId());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public class VndMediaType {
|
||||
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
||||
public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + 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_COLLECTION = PREFIX + "changesetCollection" + 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 BRANCH_COLLECTION = PREFIX + "branchCollection" + 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 = PREFIX + "repositoryType" + SUFFIX;
|
||||
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
|
||||
|
||||
@@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
|
||||
}
|
||||
else
|
||||
{
|
||||
permitted = RepositoryPermissions.read(repository).isPermitted();
|
||||
permitted = RepositoryPermissions.pull(repository).isPermitted();
|
||||
}
|
||||
|
||||
return permitted;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,5 @@ unpriv = secret
|
||||
[roles]
|
||||
admin = *
|
||||
user = something:*
|
||||
repo_read = "repository:read:1"
|
||||
repo_write = "repository:push:1"
|
||||
repo_read = "repository:read,pull:1"
|
||||
repo_write = "repository:read,write,pull,push:1"
|
||||
|
||||
@@ -16,6 +16,7 @@ import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -24,8 +25,10 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Clock;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -70,9 +73,7 @@ class XmlRepositoryDAOTest {
|
||||
Clock clock = mock(Clock.class);
|
||||
when(clock.millis()).then(ic -> atomicClock.incrementAndGet());
|
||||
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
|
||||
|
||||
return dao;
|
||||
return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -329,6 +330,21 @@ class XmlRepositoryDAOTest {
|
||||
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
|
||||
void shouldReadPathDatabaseAndMetadataOfRepositories() {
|
||||
Repository heartOfGold = createHeartOfGold();
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
<groupId>sonia.scm</groupId>
|
||||
<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>
|
||||
<name>scm-it</name>
|
||||
|
||||
@@ -91,6 +92,14 @@
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<configuration>
|
||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.mycila.maven-license-plugin</groupId>
|
||||
<artifactId>maven-license-plugin</artifactId>
|
||||
|
||||
209
scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java
Normal file
209
scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import sonia.scm.it.utils.RepositoryUtil;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.client.api.RepositoryClient;
|
||||
import sonia.scm.repository.client.api.RepositoryClientException;
|
||||
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.RestUtil.given;
|
||||
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.WRITE;
|
||||
import static sonia.scm.it.utils.TestData.callRepository;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
@@ -91,11 +93,11 @@ public class PermissionsITCase {
|
||||
public void prepareEnvironment() {
|
||||
TestData.createDefault();
|
||||
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.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType);
|
||||
TestData.createUserPermission(USER_WRITE, WRITE, repositoryType);
|
||||
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);
|
||||
createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
|
||||
}
|
||||
@@ -109,7 +111,7 @@ public class PermissionsITCase {
|
||||
|
||||
@Test
|
||||
public void readUserShouldNotSeeBruteForcePermissions() {
|
||||
given(VndMediaType.PERMISSION, USER_READ, USER_PASS)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION, USER_READ, USER_PASS)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
|
||||
.then()
|
||||
@@ -125,7 +127,7 @@ public class PermissionsITCase {
|
||||
|
||||
@Test
|
||||
public void writeUserShouldNotSeeBruteForcePermissions() {
|
||||
given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION, USER_WRITE, USER_PASS)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
|
||||
.then()
|
||||
@@ -145,7 +147,7 @@ public class PermissionsITCase {
|
||||
|
||||
@Test
|
||||
public void otherUserShouldNotSeeBruteForcePermissions() {
|
||||
given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION, USER_OTHER, USER_PASS)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
|
||||
.then()
|
||||
|
||||
@@ -4,15 +4,16 @@ import io.restassured.response.ValidatableResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObjectBuilder;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
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_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 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);
|
||||
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
|
||||
given(VndMediaType.PERMISSION)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION)
|
||||
.when()
|
||||
.content("{\n" +
|
||||
"\t\"type\": \"" + permissionType.name() + "\",\n" +
|
||||
"\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" +
|
||||
"\t\"name\": \"" + name + "\",\n" +
|
||||
"\t\"groupPermission\": false\n" +
|
||||
"\t\n" +
|
||||
@@ -106,7 +112,7 @@ public class TestData {
|
||||
}
|
||||
|
||||
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()
|
||||
.get(TestData.getDefaultPermissionUrl(username, password, repositoryType))
|
||||
.then()
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -15,6 +15,8 @@ public class GitConfigDto extends HalRepresentation {
|
||||
|
||||
private String gcExpression;
|
||||
|
||||
private boolean nonFastForwardDisallowed;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
|
||||
@@ -55,8 +55,10 @@ public class GitConfig extends RepositoryConfig {
|
||||
@XmlElement(name = "gc-expression")
|
||||
private String gcExpression;
|
||||
|
||||
public String getGcExpression()
|
||||
{
|
||||
@XmlElement(name = "disallow-non-fast-forward")
|
||||
private boolean nonFastForwardDisallowed;
|
||||
|
||||
public String getGcExpression() {
|
||||
return gcExpression;
|
||||
}
|
||||
|
||||
@@ -64,6 +66,14 @@ public class GitConfig extends RepositoryConfig {
|
||||
this.gcExpression = gcExpression;
|
||||
}
|
||||
|
||||
public boolean isNonFastForwardDisallowed() {
|
||||
return nonFastForwardDisallowed;
|
||||
}
|
||||
|
||||
public void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) {
|
||||
this.nonFastForwardDisallowed = nonFastForwardDisallowed;
|
||||
}
|
||||
|
||||
@Override
|
||||
@XmlTransient // Only for permission checks, don't serialize to XML
|
||||
public String getId() {
|
||||
|
||||
@@ -68,10 +68,13 @@ public class GitHookTagProvider implements HookTagProvider {
|
||||
|
||||
if (Strings.isNullOrEmpty(tag)){
|
||||
logger.debug("received ref name {} is not a tag", refName);
|
||||
} else if (rc.getType() == ReceiveCommand.Type.CREATE) {
|
||||
createdTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getNewId())));
|
||||
} else if (rc.getType() == ReceiveCommand.Type.DELETE){
|
||||
deletedTagBuilder.add(new Tag(tag, GitUtil.getId(rc.getOldId())));
|
||||
} else if (isCreate(rc)) {
|
||||
createdTagBuilder.add(createTagFromNewId(rc, tag));
|
||||
} else if (isDelete(rc)){
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
public List<Tag> getCreatedTags() {
|
||||
return createdTags;
|
||||
|
||||
@@ -35,79 +35,63 @@ package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.ReceivePack;
|
||||
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
|
||||
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
|
||||
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
|
||||
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.spi.HookEventFacade;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* GitReceivePackFactory creates {@link ReceivePack} objects and assigns the required
|
||||
* Hook components.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitReceivePackFactory
|
||||
implements ReceivePackFactory<HttpServletRequest>
|
||||
public class GitReceivePackFactory implements ReceivePackFactory<HttpServletRequest>
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param hookEventFacade
|
||||
* @param handler
|
||||
*/
|
||||
private final GitRepositoryHandler handler;
|
||||
|
||||
private ReceivePackFactory wrapped;
|
||||
|
||||
private final GitReceiveHook hook;
|
||||
|
||||
@Inject
|
||||
public GitReceivePackFactory(HookEventFacade hookEventFacade,
|
||||
GitRepositoryHandler handler)
|
||||
{
|
||||
hook = new GitReceiveHook(hookEventFacade, handler);
|
||||
public GitReceivePackFactory(GitRepositoryHandler handler, HookEventFacade hookEventFacade) {
|
||||
this.handler = handler;
|
||||
this.hook = new GitReceiveHook(hookEventFacade, handler);
|
||||
this.wrapped = new DefaultReceivePackFactory();
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param repository
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws ServiceNotAuthorizedException
|
||||
* @throws ServiceNotEnabledException
|
||||
*/
|
||||
@Override
|
||||
public ReceivePack create(HttpServletRequest request, Repository repository)
|
||||
throws ServiceNotEnabledException, ServiceNotAuthorizedException
|
||||
{
|
||||
ReceivePack rpack = defaultFactory.create(request, repository);
|
||||
throws ServiceNotEnabledException, ServiceNotAuthorizedException {
|
||||
ReceivePack receivePack = wrapped.create(request, repository);
|
||||
receivePack.setAllowNonFastForwards(isNonFastForwardAllowed());
|
||||
|
||||
rpack.setPreReceiveHook(hook);
|
||||
rpack.setPostReceiveHook(hook);
|
||||
receivePack.setPreReceiveHook(hook);
|
||||
receivePack.setPostReceiveHook(hook);
|
||||
// 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 */
|
||||
private DefaultReceivePackFactory defaultFactory =
|
||||
new DefaultReceivePackFactory();
|
||||
|
||||
/** Field description */
|
||||
private GitReceiveHook hook;
|
||||
@VisibleForTesting
|
||||
void setWrapped(ReceivePackFactory wrapped) {
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { InputField, Checkbox } from "@scm-manager/ui-components";
|
||||
type Configuration = {
|
||||
repositoryDirectory?: string,
|
||||
gcExpression?: string,
|
||||
nonFastForwardDisallowed: boolean,
|
||||
disabled: boolean,
|
||||
_links: Links
|
||||
}
|
||||
@@ -41,7 +42,7 @@ class GitConfigurationForm extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { gcExpression, disabled } = this.state;
|
||||
const { gcExpression, nonFastForwardDisallowed, disabled } = this.state;
|
||||
const { readOnly, t } = this.props;
|
||||
|
||||
return (
|
||||
@@ -53,6 +54,13 @@ class GitConfigurationForm extends React.Component<Props, State> {
|
||||
onChange={this.handleChange}
|
||||
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"
|
||||
label={t("scm-git-plugin.config.disabled")}
|
||||
helpText={t("scm-git-plugin.config.disabledHelpText")}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"title": "Git Configuration",
|
||||
"gcExpression": "GC Cron Expression",
|
||||
"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",
|
||||
"disabledHelpText": "Enable or disable the Git plugin",
|
||||
"submit": "Submit"
|
||||
|
||||
@@ -6,8 +6,7 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.GitConfig;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class GitConfigDtoToGitConfigMapperTest {
|
||||
@@ -21,12 +20,14 @@ public class GitConfigDtoToGitConfigMapperTest {
|
||||
GitConfig config = mapper.map(dto);
|
||||
assertEquals("express", config.getGcExpression());
|
||||
assertFalse(config.isDisabled());
|
||||
assertTrue(config.isNonFastForwardDisallowed());
|
||||
}
|
||||
|
||||
private GitConfigDto createDefaultDto() {
|
||||
GitConfigDto gitConfigDto = new GitConfigDto();
|
||||
gitConfigDto.setGcExpression("express");
|
||||
gitConfigDto.setDisabled(false);
|
||||
gitConfigDto.setNonFastForwardDisallowed(true);
|
||||
return gitConfigDto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,23 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import java.util.List;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
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.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.mockito.stubbing.OngoingStubbing;
|
||||
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}.
|
||||
*
|
||||
@@ -54,6 +57,11 @@ import sonia.scm.repository.Tag;
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class GitHookTagProviderTest {
|
||||
|
||||
private static final String ZERO = ObjectId.zeroId().getName();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Mock
|
||||
private ReceiveCommand command;
|
||||
|
||||
@@ -73,7 +81,7 @@ public class GitHookTagProviderTest {
|
||||
@Test
|
||||
public void testGetCreatedTags() {
|
||||
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());
|
||||
assertThat(provider.getDeletedTags(), empty());
|
||||
@@ -85,7 +93,7 @@ public class GitHookTagProviderTest {
|
||||
@Test
|
||||
public void testGetDeletedTags() {
|
||||
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());
|
||||
assertTag("1.0.0", revision, provider.getDeletedTags());
|
||||
@@ -97,12 +105,25 @@ public class GitHookTagProviderTest {
|
||||
@Test
|
||||
public void testWithBranch(){
|
||||
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.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){
|
||||
assertNotNull(tags);
|
||||
assertFalse(tags.isEmpty());
|
||||
@@ -112,18 +133,11 @@ public class GitHookTagProviderTest {
|
||||
assertEquals(revision, tag.getRevision());
|
||||
}
|
||||
|
||||
private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String id){
|
||||
OngoingStubbing<ObjectId> ongoing;
|
||||
if (type == ReceiveCommand.Type.CREATE){
|
||||
ongoing = when(command.getNewId());
|
||||
} else {
|
||||
ongoing = when(command.getOldId());
|
||||
}
|
||||
ongoing.thenReturn(ObjectId.fromString(id));
|
||||
|
||||
private GitHookTagProvider createProvider(ReceiveCommand.Type type, String ref, String newId, String oldId){
|
||||
when(command.getNewId()).thenReturn(ObjectId.fromString(newId));
|
||||
when(command.getOldId()).thenReturn(ObjectId.fromString(oldId));
|
||||
when(command.getType()).thenReturn(type);
|
||||
when(command.getRefName()).thenReturn(ref);
|
||||
|
||||
return new GitHookTagProvider(commands);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
<dependency>
|
||||
<groupId>com.aragost.javahg</groupId>
|
||||
<artifactId>javahg</artifactId>
|
||||
<version>0.8-scm1</version>
|
||||
<version>0.13-java7</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
@@ -81,7 +81,6 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@@ -93,19 +92,6 @@
|
||||
<url>http://maven.scm-manager.org/nexus/content/groups/public</url>
|
||||
</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>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -19,6 +19,8 @@ public class HgConfigDto extends HalRepresentation {
|
||||
private String pythonPath;
|
||||
private boolean useOptimizedBytecode;
|
||||
private boolean showRevisionInId;
|
||||
private boolean enableHttpPostArgs;
|
||||
private boolean disableHookSSLValidation;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
|
||||
@@ -58,6 +58,14 @@ public class HgConfig extends RepositoryConfig
|
||||
|
||||
//~--- 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
|
||||
*
|
||||
@@ -124,6 +132,14 @@ public class HgConfig extends RepositoryConfig
|
||||
return useOptimizedBytecode;
|
||||
}
|
||||
|
||||
public boolean isDisableHookSSLValidation() {
|
||||
return disableHookSSLValidation;
|
||||
}
|
||||
|
||||
public boolean isEnableHttpPostArgs() {
|
||||
return enableHttpPostArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -194,6 +210,10 @@ public class HgConfig extends RepositoryConfig
|
||||
this.showRevisionInId = showRevisionInId;
|
||||
}
|
||||
|
||||
public void setEnableHttpPostArgs(boolean enableHttpPostArgs) {
|
||||
this.enableHttpPostArgs = enableHttpPostArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -205,6 +225,10 @@ public class HgConfig extends RepositoryConfig
|
||||
this.useOptimizedBytecode = useOptimizedBytecode;
|
||||
}
|
||||
|
||||
public void setDisableHookSSLValidation(boolean disableHookSSLValidation) {
|
||||
this.disableHookSSLValidation = disableHookSSLValidation;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
@@ -225,10 +249,11 @@ public class HgConfig extends RepositoryConfig
|
||||
/** Field description */
|
||||
private boolean showRevisionInId = false;
|
||||
|
||||
@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;
|
||||
}
|
||||
private boolean enableHttpPostArgs = false;
|
||||
|
||||
/**
|
||||
* disable validation of ssl certificates for mercurial hook
|
||||
* @see <a href="https://goo.gl/zH5eY8">Issue 959</a>
|
||||
*/
|
||||
private boolean disableHookSSLValidation = false;
|
||||
}
|
||||
|
||||
@@ -56,15 +56,20 @@ import sonia.scm.web.cgi.CGIExecutor;
|
||||
import sonia.scm.web.cgi.CGIExecutorFactory;
|
||||
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.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -74,6 +79,8 @@ import java.util.Enumeration;
|
||||
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
{
|
||||
|
||||
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_REPOSITORY_NAME = "REPO_NAME";
|
||||
|
||||
@@ -83,6 +90,8 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
/** Field description */
|
||||
public static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
|
||||
|
||||
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_SESSION_PREFIX = "SCM_";
|
||||
|
||||
@@ -268,9 +277,20 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
directory.getAbsolutePath());
|
||||
|
||||
// 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-
|
||||
HgEnvironment.prepareEnvironment(
|
||||
executor.getEnvironment().asMutableMap(),
|
||||
environment,
|
||||
handler,
|
||||
hookManager,
|
||||
request
|
||||
|
||||
@@ -33,13 +33,23 @@
|
||||
|
||||
package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
||||
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 java.util.Set;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 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");
|
||||
|
||||
public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate)
|
||||
private final HgRepositoryHandler repositoryHandler;
|
||||
|
||||
public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate, HgRepositoryHandler repositoryHandler)
|
||||
{
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ import javax.inject.Inject;
|
||||
public class HgPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory {
|
||||
|
||||
private final ScmConfiguration configuration;
|
||||
private final HgRepositoryHandler repositoryHandler;
|
||||
|
||||
@Inject
|
||||
public HgPermissionFilterFactory(ScmConfiguration configuration) {
|
||||
public HgPermissionFilterFactory(ScmConfiguration configuration, HgRepositoryHandler repositoryHandler) {
|
||||
this.configuration = configuration;
|
||||
this.repositoryHandler = repositoryHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -25,6 +27,6 @@ public class HgPermissionFilterFactory implements ScmProviderHttpServletDecorato
|
||||
|
||||
@Override
|
||||
public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) {
|
||||
return new HgPermissionFilter(configuration, delegate);
|
||||
return new HgPermissionFilter(configuration, delegate,repositoryHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ type Configuration = {
|
||||
"encoding": string,
|
||||
"useOptimizedBytecode": boolean,
|
||||
"showRevisionInId": boolean,
|
||||
"disableHookSSLValidation": boolean,
|
||||
"enableHttpPostArgs": boolean,
|
||||
"disabled": boolean,
|
||||
"_links": Links
|
||||
};
|
||||
@@ -101,6 +103,8 @@ class HgConfigurationForm extends React.Component<Props, State> {
|
||||
{this.inputField("encoding")}
|
||||
{this.checkbox("useOptimizedBytecode")}
|
||||
{this.checkbox("showRevisionInId")}
|
||||
{this.checkbox("disableHookSSLValidation")}
|
||||
{this.checkbox("enableHttpPostArgs")}
|
||||
{this.checkbox("disabled")}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
"useOptimizedBytecodeHelpText": "Use the Python '-O' switch.",
|
||||
"showRevisionInId": "Show Revision",
|
||||
"showRevisionInIdHelpText": "Show revision as part of the node id.",
|
||||
"enableHttpPostArgs": "Enable HttpPostArgs Protocol",
|
||||
"enableHttpPostArgsHelpText": "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.",
|
||||
"disableHookSSLValidation": "Disable SSL Validation on Hooks",
|
||||
"disableHookSSLValidationHelpText": "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.",
|
||||
"disabled": "Disabled",
|
||||
"disabledHelpText": "Enable or disable the Mercurial plugin.",
|
||||
"required": "This configuration value is required"
|
||||
|
||||
@@ -37,7 +37,22 @@ from collections import defaultdict
|
||||
from mercurial import cmdutil,util
|
||||
|
||||
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>'
|
||||
|
||||
@@ -201,7 +216,7 @@ class File_Printer:
|
||||
description = 'n/a'
|
||||
if not self.disableLastCommit:
|
||||
linkrev = self.repo[file.linkrev()]
|
||||
date = '%d %d' % util.parsedate(linkrev.date())
|
||||
date = '%d %d' % _parsedate(linkrev.date())
|
||||
description = linkrev.description()
|
||||
format = '%s %i %s %s\n'
|
||||
if self.transport:
|
||||
|
||||
@@ -36,7 +36,11 @@ from mercurial.hgweb import hgweb, wsgicgi
|
||||
|
||||
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', 'allow_read', '*')
|
||||
@@ -45,7 +49,13 @@ u.setconfig('web', 'allow_push', '*')
|
||||
u.setconfig('hooks', 'changegroup.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'])
|
||||
application = hgweb(r)
|
||||
wsgicgi.launch(application)
|
||||
|
||||
@@ -47,7 +47,7 @@ def printMessages(ui, msgs):
|
||||
for line in msgs:
|
||||
if line.startswith("_e") or line.startswith("_n"):
|
||||
line = line[2:];
|
||||
ui.warn(line);
|
||||
ui.warn('%s\n' % line.rstrip())
|
||||
|
||||
def callHookUrl(ui, repo, hooktype, node):
|
||||
abort = True
|
||||
@@ -79,8 +79,10 @@ def callHookUrl(ui, repo, hooktype, node):
|
||||
printMessages(ui, msg.splitlines(True))
|
||||
else:
|
||||
ui.warn( "ERROR: scm-hook failed with an unknown error\n" )
|
||||
ui.traceback()
|
||||
except ValueError:
|
||||
ui.warn( "scm-hook failed with an exception\n" )
|
||||
ui.traceback()
|
||||
return abort
|
||||
|
||||
def callback(ui, repo, hooktype, node=None, source=None, pending=None, **kwargs):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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"
|
||||
file_add = "a {file_add}\n"
|
||||
file_mod = "m {file_mod}\n"
|
||||
file_del = "d {file_del}\n"
|
||||
extra = "{key}={value|stringescape},"
|
||||
footer = "%{pattern}"
|
||||
@@ -30,6 +30,8 @@ public class HgConfigDtoToHgConfigMapperTest {
|
||||
assertEquals("/etc/", config.getPythonPath());
|
||||
assertTrue(config.isShowRevisionInId());
|
||||
assertTrue(config.isUseOptimizedBytecode());
|
||||
assertTrue(config.isDisableHookSSLValidation());
|
||||
assertTrue(config.isEnableHttpPostArgs());
|
||||
}
|
||||
|
||||
private HgConfigDto createDefaultDto() {
|
||||
@@ -41,6 +43,8 @@ public class HgConfigDtoToHgConfigMapperTest {
|
||||
configDto.setPythonPath("/etc/");
|
||||
configDto.setShowRevisionInId(true);
|
||||
configDto.setUseOptimizedBytecode(true);
|
||||
configDto.setDisableHookSSLValidation(true);
|
||||
configDto.setEnableHttpPostArgs(true);
|
||||
|
||||
return configDto;
|
||||
}
|
||||
|
||||
@@ -31,18 +31,29 @@
|
||||
|
||||
package sonia.scm.web;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.HgConfig;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
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}.
|
||||
*
|
||||
@@ -60,9 +71,33 @@ public class HgPermissionFilterTest {
|
||||
@Mock
|
||||
private RepositoryProvider repositoryProvider;
|
||||
|
||||
@Mock
|
||||
private HgRepositoryHandler hgRepositoryHandler;
|
||||
|
||||
private WireProtocolRequestMockFactory wireProtocol = new WireProtocolRequestMockFactory("/scm/hg/repo");
|
||||
|
||||
@InjectMocks
|
||||
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)}.
|
||||
*/
|
||||
@@ -81,8 +116,121 @@ public class HgPermissionFilterTest {
|
||||
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) {
|
||||
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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -64,7 +64,6 @@
|
||||
<assembleDirectory>${exploded.directory}</assembleDirectory>
|
||||
<repoPath>lib</repoPath>
|
||||
<repositoryLayout>flat</repositoryLayout>
|
||||
<includeConfigurationDirectoryInClasspath>true</includeConfigurationDirectoryInClasspath>
|
||||
<daemons>
|
||||
<daemon>
|
||||
|
||||
@@ -306,8 +305,8 @@
|
||||
</profiles>
|
||||
|
||||
<properties>
|
||||
<commons.daemon.version>1.0.15</commons.daemon.version>
|
||||
<commons.daemon.native.version>1.0.15.1</commons.daemon.native.version>
|
||||
<commons.daemon.version>1.1.0</commons.daemon.version>
|
||||
<commons.daemon.native.version>1.1.0</commons.daemon.native.version>
|
||||
<exploded.directory>${project.build.directory}/appassembler/commons-daemon/scm-server</exploded.directory>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
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 classNames from "classnames";
|
||||
import DropDown from "./forms/DropDown";
|
||||
@@ -39,7 +39,9 @@ class BranchSelector extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1,7 +1,8 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import SubmitButton, { type ButtonProps } from "./SubmitButton";
|
||||
import { type ButtonProps } from "./Button";
|
||||
import SubmitButton from "./SubmitButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
|
||||
@@ -5,6 +5,9 @@ export { default as Button } from "./Button.js";
|
||||
export { default as CreateButton } from "./CreateButton.js";
|
||||
export { default as DeleteButton } from "./DeleteButton.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 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";
|
||||
|
||||
@@ -54,7 +54,7 @@ class Select extends React.Component<Props> {
|
||||
>
|
||||
{options.map(opt => {
|
||||
return (
|
||||
<option value={opt.value} key={opt.value}>
|
||||
<option value={opt.value} key={"KEY_" + opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
export type RepositoryRole = {
|
||||
name: string,
|
||||
verbs: string[]
|
||||
};
|
||||
|
||||
export type AvailableRepositoryPermissions = {
|
||||
availableVerbs: string[],
|
||||
availableRoles: RepositoryRole[]
|
||||
};
|
||||
@@ -7,7 +7,7 @@ export type Permission = PermissionCreateEntry & {
|
||||
|
||||
export type PermissionCreateEntry = {
|
||||
name: string,
|
||||
type: string,
|
||||
verbs: string[],
|
||||
groupPermission: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -24,3 +24,5 @@ export type { Permission, PermissionCreateEntry, PermissionCollection } from "./
|
||||
export type { SubRepository, File } from "./Sources";
|
||||
|
||||
export type { SelectValue, AutocompleteObject } from "./Autocomplete";
|
||||
|
||||
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";
|
||||
|
||||
72
scm-ui/public/locales/de/commons.json
Normal file
72
scm-ui/public/locales/de/commons.json
Normal 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": "Plaintext 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"
|
||||
}
|
||||
}
|
||||
93
scm-ui/public/locales/de/config.json
Normal file
93
scm-ui/public/locales/de/config.json
Normal 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 Anmeldeversuche"
|
||||
},
|
||||
"general-settings": {
|
||||
"realm-description": "Realm Beschreibung",
|
||||
"enable-repository-archive": "Repository Archiv aktivieren",
|
||||
"disable-grouping-grid": "Gruppen deaktiviern",
|
||||
"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 aktiviern",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
70
scm-ui/public/locales/de/groups.json
Normal file
70
scm-ui/public/locales/de/groups.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"group": {
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"creationDate": "Erstellt",
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"type": "Typ",
|
||||
"members": "Mitglieder"
|
||||
},
|
||||
"groups": {
|
||||
"title": "Gruppen",
|
||||
"subtitle": "Verwaltung der Gruppen"
|
||||
},
|
||||
"single-group": {
|
||||
"error-title": "Fehler",
|
||||
"error-subtitle": "Unbekannter Gruppen Fehler",
|
||||
"navigation-label": "Navigation",
|
||||
"actions-label": "Aktionen",
|
||||
"information-label": "Informationen",
|
||||
"back-label": "Zurück"
|
||||
},
|
||||
"add-group": {
|
||||
"title": "Gruppe erstellen",
|
||||
"subtitle": "Erstllen einer neuen Gruppe"
|
||||
},
|
||||
"create-group-button": {
|
||||
"label": "Erstellen"
|
||||
},
|
||||
"edit-group-button": {
|
||||
"label": "Bearbeiten"
|
||||
},
|
||||
"add-member-button": {
|
||||
"label": "Mitglied hinzufügen"
|
||||
},
|
||||
"remove-member-button": {
|
||||
"label": "Mitglied entfernen"
|
||||
},
|
||||
"add-member-textfield": {
|
||||
"label": "Mitglied hinzufügen",
|
||||
"error": "Ungültiger Name für Mitglied"
|
||||
},
|
||||
"add-member-autocomplete": {
|
||||
"placeholder": "Benutzername eingeben",
|
||||
"loading": "suche...",
|
||||
"no-options": "Kein Vorschlag für Benutzername verfügbar"
|
||||
},
|
||||
|
||||
"group-form": {
|
||||
"submit": "Speichern",
|
||||
"name-error": "Name ist ungültig",
|
||||
"description-error": "Beschreibung ist ungültig",
|
||||
"help": {
|
||||
"nameHelpText": "Einzigartiger Name der Gruppe",
|
||||
"descriptionHelpText": "Eine kurze Beschreibung der Gruppe",
|
||||
"memberHelpText": "Benutzername des Mitglieds der Gruppe"
|
||||
}
|
||||
},
|
||||
"delete-group-button": {
|
||||
"label": "Löschen",
|
||||
"confirm-alert": {
|
||||
"title": "Gruppe löschen",
|
||||
"message": "Soll die Gruppe wirklich gelöscht werden?",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"set-permissions-button": {
|
||||
"label": "Berechtigungen ändern"
|
||||
}
|
||||
}
|
||||
8
scm-ui/public/locales/de/permissions.json
Normal file
8
scm-ui/public/locales/de/permissions.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"form": {
|
||||
"submit-button": {
|
||||
"label": "Berechtigungen speichern"
|
||||
},
|
||||
"set-permissions-successful": "Berechtigungen erfolgreich gespeichert"
|
||||
}
|
||||
}
|
||||
147
scm-ui/public/locales/de/repos.json
Normal file
147
scm-ui/public/locales/de/repos.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"repository": {
|
||||
"name": "Name",
|
||||
"type": "Typ",
|
||||
"contact": "Kontakt",
|
||||
"description": "Beschreibung",
|
||||
"creationDate": "Erstellt",
|
||||
"lastModified": "Zuletzt bearbeitet"
|
||||
},
|
||||
"validation": {
|
||||
"name-invalid": "Der Name des Repositories ist ungültig",
|
||||
"contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Repositories",
|
||||
"subtitle": "Übersicht aller verfügbaren Repositories",
|
||||
"create-button": "Erstellen"
|
||||
},
|
||||
"repository-root": {
|
||||
"error-title": "Fehler",
|
||||
"error-subtitle": "Unbekannter Repository Fehler",
|
||||
"actions-label": "Aktionen",
|
||||
"back-label": "Zurück",
|
||||
"navigation-label": "Navigation",
|
||||
"history": "Commits",
|
||||
"information": "Informationen",
|
||||
"permissions": "Berechtigungen",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"create": {
|
||||
"title": "Repository Erstellen",
|
||||
"subtitle": "Erstellen eines neuen Repositories"
|
||||
},
|
||||
"repository-form": {
|
||||
"submit": "Speichern"
|
||||
},
|
||||
"edit-nav-link": {
|
||||
"label": "Bearbeiten"
|
||||
},
|
||||
"delete-nav-action": {
|
||||
"label": "Löschen",
|
||||
"confirm-alert": {
|
||||
"title": "Repository löschen",
|
||||
"message": "Soll das Repository wirklich gelöscht werden?",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"file-tree": {
|
||||
"name": "Name",
|
||||
"length": "Größe",
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"description": "Beschreibung",
|
||||
"branch": "Branch"
|
||||
},
|
||||
"content": {
|
||||
"historyButton": "History",
|
||||
"sourcesButton": "Sources",
|
||||
"downloadButton": "Download",
|
||||
"path": "Pfad",
|
||||
"branch": "Branch",
|
||||
"lastModified": "Zuletzt bearbeitet",
|
||||
"description": "Beschreibung",
|
||||
"size": "Größe"
|
||||
}
|
||||
},
|
||||
"changesets": {
|
||||
"diff": {
|
||||
"not-supported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt"
|
||||
},
|
||||
"error-title": "Fehler",
|
||||
"error-subtitle": "Changesets konnten nicht abgerufen werden",
|
||||
"changeset": {
|
||||
"id": "ID",
|
||||
"description": "Beschreibung",
|
||||
"contact": "Kontakt",
|
||||
"date": "Datum",
|
||||
"summary": "Changeset {{id}} wurde committet {{time}}"
|
||||
},
|
||||
"author": {
|
||||
"name": "Autor",
|
||||
"mail": "Mail"
|
||||
}
|
||||
},
|
||||
"branch-selector": {
|
||||
"label": "Branches"
|
||||
},
|
||||
"permission": {
|
||||
"user": "Benutzer",
|
||||
"group": "Gruppe",
|
||||
"error-title": "Fehler",
|
||||
"error-subtitle": "Unbekannter Fehler bei Berechtigung",
|
||||
"name": "Benutzer oder Gruppe",
|
||||
"role": "Rolle",
|
||||
"permissions": "Berechtigung",
|
||||
"group-permission": "Gruppenberechtigung",
|
||||
"user-permission": "Benutzerberechtigung",
|
||||
"edit-permission": {
|
||||
"delete-button": "Löschen",
|
||||
"save-button": "Änderungen speichern"
|
||||
},
|
||||
"advanced-button": {
|
||||
"label": "Erweitert"
|
||||
},
|
||||
"delete-permission-button": {
|
||||
"label": "Löschen",
|
||||
"confirm-alert": {
|
||||
"title": "Berechtigung löschen",
|
||||
"message": "Soll die Berechtigung wirklich gelöscht werden?",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"add-permission": {
|
||||
"add-permission-heading": "Neue Berechtigung hinzufügen",
|
||||
"submit-button": "Speichern",
|
||||
"name-input-invalid": "Die Berechtigung darf nicht leer sein! Falls sie nicht leer ist, ist der Name ungültig oder die Berechtigung besteht bereits!"
|
||||
},
|
||||
"help": {
|
||||
"groupPermissionHelpText": "Zeigt ob es sich bei der Berechtigung um eine Gruppenberechtigung handelt. Wenn hier kein Haken gesetzt ist, handelt es sich um eine Benutzerberechtigung.",
|
||||
"nameHelpText": "Verwaltung von Berechtigungen für Benutzer und Gruppen",
|
||||
"roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird den Erweitert-Button benutzen um Details zu sehen.",
|
||||
"permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden."
|
||||
},
|
||||
"autocomplete": {
|
||||
"no-group-options": "Kein Gruppenname als Vorschlag verfügbar",
|
||||
"group-placeholder": "Gruppe eingeben",
|
||||
"no-user-options": "Kein Benutzername als Vorschlag verfügbar",
|
||||
"user-placeholder": "Benutzer eingeben",
|
||||
"loading": "suche..."
|
||||
},
|
||||
"advanced": {
|
||||
"dialog": {
|
||||
"title": "Erweiterte Berechtigungen",
|
||||
"submit": "Speichern",
|
||||
"abort": "Abbrechen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"nameHelpText": "Der Name des Repositories. Dieser wird Teil der URL des Repositories sein.",
|
||||
"typeHelpText": "Der Typ des Repositories (Mercurial, Git oder Subversion).",
|
||||
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",
|
||||
"descriptionHelpText": "Eine kurze Beschreibung des Repositories."
|
||||
}
|
||||
}
|
||||
68
scm-ui/public/locales/de/users.json
Normal file
68
scm-ui/public/locales/de/users.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"user": {
|
||||
"name": "Benutzername",
|
||||
"displayName": "Anzeigename",
|
||||
"mail": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"admin": "Admin",
|
||||
"active": "Aktiv",
|
||||
"type": "Typ",
|
||||
"creationDate": "Erstell",
|
||||
"lastModified": "Zuletzt bearbeitet"
|
||||
},
|
||||
"users": {
|
||||
"title": "Benutzer",
|
||||
"subtitle": "Verwaltung der Benutzer"
|
||||
},
|
||||
"create-user-button": {
|
||||
"label": "Erstellen"
|
||||
},
|
||||
"delete-user-button": {
|
||||
"label": "Löschen",
|
||||
"confirm-alert": {
|
||||
"title": "Benutzer löschen",
|
||||
"message": "Soll der Benutzer wirklich gelöscht werden?",
|
||||
"submit": "Ja",
|
||||
"cancel": "Nein"
|
||||
}
|
||||
},
|
||||
"edit-user-button": {
|
||||
"label": "Bearbeiten"
|
||||
},
|
||||
"set-password-button": {
|
||||
"label": "Passwort ändern"
|
||||
},
|
||||
"set-permissions-button": {
|
||||
"label": "Berechtigungen ändern"
|
||||
},
|
||||
"user-form": {
|
||||
"submit": "Speichern"
|
||||
},
|
||||
"add-user": {
|
||||
"title": "Benutzer erstellen",
|
||||
"subtitle": "Erstellen eines neuen Benutzers"
|
||||
},
|
||||
"single-user": {
|
||||
"error-title": "Fehler",
|
||||
"error-subtitle": "Unbekannter Benutzer Fehler",
|
||||
"navigation-label": "Navigation",
|
||||
"actions-label": "Aktionen",
|
||||
"information-label": "Informationen",
|
||||
"back-label": "Zurück"
|
||||
},
|
||||
"validation": {
|
||||
"mail-invalid": "Diese E-Mail ist ungültig",
|
||||
"name-invalid": "Dieser Name ist ungültig",
|
||||
"displayname-invalid": "Dieser Anzeigename ist ungültig"
|
||||
},
|
||||
"password": {
|
||||
"set-password-successful": "Das Passwort wurde erfolgreich gespeichert."
|
||||
},
|
||||
"help": {
|
||||
"usernameHelpText": "Einzigartiger Name des Benutzers.",
|
||||
"displayNameHelpText": "Anzeigename des Benutzers.",
|
||||
"mailHelpText": "E-Mail Adresse des Benutzers.",
|
||||
"adminHelpText": "Ein Administrator kann Repositories, Gruppen und Benutzer erstellen, bearbeiten und löschen.",
|
||||
"activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers."
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,6 @@
|
||||
"passwordInvalid": "Password has to be between 6 and 32 characters",
|
||||
"passwordConfirmFailed": "Passwords have to be identical",
|
||||
"submit": "Submit",
|
||||
"changedSuccessfully": "Pasword successfully changed"
|
||||
"changedSuccessfully": "Password successfully changed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +91,18 @@
|
||||
"group": "Group",
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown permissions error",
|
||||
"name": "User or Group",
|
||||
"type": "Type",
|
||||
"name": "User or group",
|
||||
"role": "Role",
|
||||
"permissions": "Permissions",
|
||||
"group-permission": "Group Permission",
|
||||
"user-permission": "User Permission",
|
||||
"edit-permission": {
|
||||
"delete-button": "Delete",
|
||||
"save-button": "Save Changes"
|
||||
},
|
||||
"advanced-button": {
|
||||
"label": "Advanced"
|
||||
},
|
||||
"delete-permission-button": {
|
||||
"label": "Delete",
|
||||
"confirm-alert": {
|
||||
@@ -114,9 +118,10 @@
|
||||
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
|
||||
},
|
||||
"help": {
|
||||
"groupPermissionHelpText": "States if a permission is a group permission.",
|
||||
"groupPermissionHelpText": "States if a permission is a group permission. If this is not checked, it is a user permission.",
|
||||
"nameHelpText": "Manage permissions for a specific user or group",
|
||||
"typeHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions"
|
||||
"roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.",
|
||||
"permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles"
|
||||
},
|
||||
"autocomplete": {
|
||||
"no-group-options": "No group suggestion available",
|
||||
@@ -124,6 +129,13 @@
|
||||
"no-user-options": "No user suggestion available",
|
||||
"user-placeholder": "Enter user",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"advanced": {
|
||||
"dialog": {
|
||||
"title": "Advanced permissions",
|
||||
"submit": "Submit",
|
||||
"abort": "Abort"
|
||||
}
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Checkbox } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
disabled: boolean,
|
||||
name: string,
|
||||
checked: boolean,
|
||||
onChange?: (value: boolean, name?: string) => void
|
||||
};
|
||||
|
||||
class PermissionCheckbox extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Checkbox
|
||||
key={this.props.name}
|
||||
name={this.props.name}
|
||||
helpText={t("verbs.repository." + this.props.name + ".description")}
|
||||
label={t("verbs.repository." + this.props.name + ".displayName")}
|
||||
checked={this.props.checked}
|
||||
onChange={this.props.onChange}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(PermissionCheckbox);
|
||||
55
scm-ui/src/repos/permissions/components/RoleSelector.js
Normal file
55
scm-ui/src/repos/permissions/components/RoleSelector.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
availableRoles: string[],
|
||||
handleRoleChange: string => void,
|
||||
role: string,
|
||||
label?: string,
|
||||
helpText?: string,
|
||||
loading?: boolean
|
||||
};
|
||||
|
||||
class RoleSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
availableRoles,
|
||||
role,
|
||||
handleRoleChange,
|
||||
loading,
|
||||
label,
|
||||
helpText
|
||||
} = this.props;
|
||||
|
||||
if (!availableRoles) return null;
|
||||
|
||||
const options = role
|
||||
? this.createSelectOptions(availableRoles)
|
||||
: ["", ...this.createSelectOptions(availableRoles)];
|
||||
|
||||
return (
|
||||
<Select
|
||||
onChange={handleRoleChange}
|
||||
value={role ? role : ""}
|
||||
options={options}
|
||||
loading={loading}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createSelectOptions(roles: string[]) {
|
||||
return roles.map(role => {
|
||||
return {
|
||||
label: role,
|
||||
value: role
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(RoleSelector);
|
||||
@@ -1,42 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
handleTypeChange: string => void,
|
||||
type: string,
|
||||
label?: string,
|
||||
helpText?: string,
|
||||
loading?: boolean
|
||||
};
|
||||
|
||||
class TypeSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const { type, handleTypeChange, loading, label, helpText } = this.props;
|
||||
const types = ["READ", "OWNER", "WRITE"];
|
||||
|
||||
return (
|
||||
<Select
|
||||
onChange={handleTypeChange}
|
||||
value={type ? type : "READ"}
|
||||
options={this.createSelectOptions(types)}
|
||||
loading={loading}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createSelectOptions(types: string[]) {
|
||||
return types.map(type => {
|
||||
return {
|
||||
label: type,
|
||||
value: type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(TypeSelector);
|
||||
@@ -18,7 +18,8 @@ describe("permission validation", () => {
|
||||
name: "PermissionName",
|
||||
groupPermission: true,
|
||||
type: "READ",
|
||||
_links: {}
|
||||
_links: {},
|
||||
verbs: []
|
||||
}
|
||||
];
|
||||
const name = "PermissionName";
|
||||
@@ -35,7 +36,8 @@ describe("permission validation", () => {
|
||||
name: "PermissionName",
|
||||
groupPermission: false,
|
||||
type: "READ",
|
||||
_links: {}
|
||||
_links: {},
|
||||
verbs: []
|
||||
}
|
||||
];
|
||||
const name = "PermissionName";
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Button, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import PermissionCheckbox from "../components/PermissionCheckbox";
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean,
|
||||
availableVerbs: string[],
|
||||
selectedVerbs: string[],
|
||||
onSubmit: (string[]) => void,
|
||||
onClose: () => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
verbs: any
|
||||
};
|
||||
|
||||
class AdvancedPermissionsDialog extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const verbs = {};
|
||||
props.availableVerbs.forEach(
|
||||
verb => (verbs[verb] = props.selectedVerbs.includes(verb))
|
||||
);
|
||||
|
||||
this.state = { verbs };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, onClose, readOnly } = this.props;
|
||||
const { verbs } = this.state;
|
||||
|
||||
const verbSelectBoxes = Object.entries(verbs).map(e => (
|
||||
<PermissionCheckbox
|
||||
disabled={readOnly}
|
||||
name={e[0]}
|
||||
checked={e[1]}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
));
|
||||
|
||||
const submitButton = !readOnly ? (
|
||||
<SubmitButton label={t("permission.advanced.dialog.submit")} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={"modal is-active"}>
|
||||
<div className="modal-background" />
|
||||
<div className="modal-card">
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">
|
||||
{t("permission.advanced.dialog.title")}
|
||||
</p>
|
||||
<button
|
||||
className="delete"
|
||||
aria-label="close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<div className="content">{verbSelectBoxes}</div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{submitButton}
|
||||
<Button
|
||||
label={t("permission.advanced.dialog.abort")}
|
||||
action={onClose}
|
||||
/>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleChange = (value: boolean, name: string) => {
|
||||
const { verbs } = this.state;
|
||||
const newVerbs = { ...verbs, [name]: value };
|
||||
this.setState({ verbs: newVerbs });
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.onSubmit(
|
||||
Object.entries(this.state.verbs)
|
||||
.filter(e => e[1])
|
||||
.map(e => e[0])
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("repos")(AdvancedPermissionsDialog);
|
||||
@@ -1,17 +1,26 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Autocomplete, Radio, SubmitButton } from "@scm-manager/ui-components";
|
||||
import TypeSelector from "./TypeSelector";
|
||||
import {
|
||||
Autocomplete,
|
||||
SubmitButton,
|
||||
Button,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
import RoleSelector from "../components/RoleSelector";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry,
|
||||
SelectValue
|
||||
} from "@scm-manager/ui-types";
|
||||
import * as validator from "./permissionValidation";
|
||||
import * as validator from "../components/permissionValidation";
|
||||
import { findMatchingRoleName } from "../modules/permissions";
|
||||
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
createPermission: (permission: PermissionCreateEntry) => void,
|
||||
loading: boolean,
|
||||
currentPermissions: PermissionCollection,
|
||||
@@ -21,10 +30,11 @@ type Props = {
|
||||
|
||||
type State = {
|
||||
name: string,
|
||||
type: string,
|
||||
verbs: string[],
|
||||
groupPermission: boolean,
|
||||
valid: boolean,
|
||||
value?: SelectValue
|
||||
value?: SelectValue,
|
||||
showAdvancedDialog: boolean
|
||||
};
|
||||
|
||||
class CreatePermissionForm extends React.Component<Props, State> {
|
||||
@@ -33,10 +43,11 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
name: "",
|
||||
type: "READ",
|
||||
verbs: props.availablePermissions.availableRoles[0].verbs,
|
||||
groupPermission: false,
|
||||
valid: true,
|
||||
value: undefined
|
||||
value: undefined,
|
||||
showAdvancedDialog: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,9 +132,23 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading } = this.props;
|
||||
const { t, availablePermissions, loading } = this.props;
|
||||
|
||||
const { type } = this.state;
|
||||
const { verbs, showAdvancedDialog } = this.state;
|
||||
|
||||
const availableRoleNames = availablePermissions.availableRoles.map(
|
||||
r => r.name
|
||||
);
|
||||
const matchingRole = findMatchingRoleName(availablePermissions, verbs);
|
||||
|
||||
const advancedDialog = showAdvancedDialog ? (
|
||||
<AdvancedPermissionsDialog
|
||||
availableVerbs={availablePermissions.availableVerbs}
|
||||
selectedVerbs={verbs}
|
||||
onClose={this.closeAdvancedPermissionsDialog}
|
||||
onSubmit={this.submitAdvancedPermissionsDialog}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -131,33 +156,58 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
<h2 className="subtitle">
|
||||
{t("permission.add-permission.add-permission-heading")}
|
||||
</h2>
|
||||
{advancedDialog}
|
||||
<form onSubmit={this.submit}>
|
||||
<Radio
|
||||
<div className="control">
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="permission_scope"
|
||||
value="USER_PERMISSION"
|
||||
checked={!this.state.groupPermission}
|
||||
label={t("permission.user-permission")}
|
||||
value="USER_PERMISSION"
|
||||
onChange={this.permissionScopeChanged}
|
||||
/>
|
||||
<Radio
|
||||
{t("permission.user-permission")}
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="permission_scope"
|
||||
value="GROUP_PERMISSION"
|
||||
checked={this.state.groupPermission}
|
||||
label={t("permission.group-permission")}
|
||||
onChange={this.permissionScopeChanged}
|
||||
/>
|
||||
{t("permission.group-permission")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<div className="column is-two-thirds">
|
||||
{this.renderAutocompletionField()}
|
||||
</div>
|
||||
<div className="column is-one-quarter">
|
||||
<TypeSelector
|
||||
label={t("permission.type")}
|
||||
helpText={t("permission.help.typeHelpText")}
|
||||
handleTypeChange={this.handleTypeChange}
|
||||
type={type ? type : "READ"}
|
||||
<div className="column is-one-third">
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<RoleSelector
|
||||
availableRoles={availableRoleNames}
|
||||
label={t("permission.role")}
|
||||
helpText={t("permission.help.roleHelpText")}
|
||||
handleRoleChange={this.handleRoleChange}
|
||||
role={matchingRole}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.permissions")}
|
||||
helpText={t("permission.help.permissionsHelpText")}
|
||||
/>
|
||||
<Button
|
||||
label={t("permission.advanced-button.label")}
|
||||
action={this.handleDetailedPermissionsPressed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
@@ -173,10 +223,25 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
handleDetailedPermissionsPressed = () => {
|
||||
this.setState({ showAdvancedDialog: true });
|
||||
};
|
||||
|
||||
closeAdvancedPermissionsDialog = () => {
|
||||
this.setState({ showAdvancedDialog: false });
|
||||
};
|
||||
|
||||
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
|
||||
this.setState({
|
||||
showAdvancedDialog: false,
|
||||
verbs: newVerbs
|
||||
});
|
||||
};
|
||||
|
||||
submit = e => {
|
||||
this.props.createPermission({
|
||||
name: this.state.name,
|
||||
type: this.state.type,
|
||||
verbs: this.state.verbs,
|
||||
groupPermission: this.state.groupPermission
|
||||
});
|
||||
this.removeState();
|
||||
@@ -186,17 +251,24 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
removeState = () => {
|
||||
this.setState({
|
||||
name: "",
|
||||
type: "READ",
|
||||
verbs: this.props.availablePermissions.availableRoles[0].verbs,
|
||||
groupPermission: false,
|
||||
valid: true
|
||||
});
|
||||
};
|
||||
|
||||
handleTypeChange = (type: string) => {
|
||||
handleRoleChange = (role: string) => {
|
||||
const selectedRole = this.findAvailableRole(role);
|
||||
this.setState({
|
||||
type: type
|
||||
verbs: selectedRole.verbs
|
||||
});
|
||||
};
|
||||
|
||||
findAvailableRole = (roleName: string) => {
|
||||
return this.props.availablePermissions.availableRoles.find(
|
||||
role => role.name === roleName
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("repos")(CreatePermissionForm);
|
||||
@@ -3,8 +3,12 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
fetchAvailablePermissionsIfNeeded,
|
||||
fetchPermissions,
|
||||
getFetchAvailablePermissionsFailure,
|
||||
getAvailablePermissions,
|
||||
getFetchPermissionsFailure,
|
||||
isFetchAvailablePermissionsPending,
|
||||
isFetchPermissionsPending,
|
||||
getPermissionsOfRepo,
|
||||
hasCreatePermission,
|
||||
@@ -17,14 +21,19 @@ import {
|
||||
modifyPermissionReset,
|
||||
deletePermissionReset
|
||||
} from "../modules/permissions";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import {
|
||||
Loading,
|
||||
ErrorPage,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
Permission,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry
|
||||
} from "@scm-manager/ui-types";
|
||||
import SinglePermission from "./SinglePermission";
|
||||
import CreatePermissionForm from "../components/CreatePermissionForm";
|
||||
import CreatePermissionForm from "./CreatePermissionForm";
|
||||
import type { History } from "history";
|
||||
import { getPermissionsLink } from "../../modules/repos";
|
||||
import {
|
||||
@@ -33,6 +42,7 @@ import {
|
||||
} from "../../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
loading: boolean,
|
||||
@@ -45,6 +55,7 @@ type Props = {
|
||||
userAutoCompleteLink: string,
|
||||
|
||||
//dispatch functions
|
||||
fetchAvailablePermissionsIfNeeded: () => void,
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
|
||||
createPermission: (
|
||||
link: string,
|
||||
@@ -65,6 +76,7 @@ type Props = {
|
||||
class Permissions extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const {
|
||||
fetchAvailablePermissionsIfNeeded,
|
||||
fetchPermissions,
|
||||
namespace,
|
||||
repoName,
|
||||
@@ -77,6 +89,7 @@ class Permissions extends React.Component<Props> {
|
||||
createPermissionReset(namespace, repoName);
|
||||
modifyPermissionReset(namespace, repoName);
|
||||
deletePermissionReset(namespace, repoName);
|
||||
fetchAvailablePermissionsIfNeeded();
|
||||
fetchPermissions(permissionsLink, namespace, repoName);
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ class Permissions extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
availablePermissions,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
@@ -112,12 +126,13 @@ class Permissions extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !permissions) {
|
||||
if (loading || !permissions || !availablePermissions) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const createPermissionForm = hasPermissionToCreate ? (
|
||||
<CreatePermissionForm
|
||||
availablePermissions={availablePermissions}
|
||||
createPermission={permission => this.createPermission(permission)}
|
||||
loading={loadingCreatePermission}
|
||||
currentPermissions={permissions}
|
||||
@@ -131,11 +146,30 @@ class Permissions extends React.Component<Props> {
|
||||
<table className="has-background-light table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("permission.name")}</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("permission.group-permission")}
|
||||
<th>
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.name")}
|
||||
helpText={t("permission.help.nameHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.group-permission")}
|
||||
helpText={t("permission.help.groupPermissionHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.role")}
|
||||
helpText={t("permission.help.roleHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.permissions")}
|
||||
helpText={t("permission.help.permissionsHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th>{t("permission.type")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -143,6 +177,7 @@ class Permissions extends React.Component<Props> {
|
||||
{permissions.map(permission => {
|
||||
return (
|
||||
<SinglePermission
|
||||
availablePermissions={availablePermissions}
|
||||
key={permission.name + permission.groupPermission.toString()}
|
||||
namespace={namespace}
|
||||
repoName={repoName}
|
||||
@@ -165,8 +200,11 @@ const mapStateToProps = (state, ownProps) => {
|
||||
getFetchPermissionsFailure(state, namespace, repoName) ||
|
||||
getCreatePermissionFailure(state, namespace, repoName) ||
|
||||
getDeletePermissionsFailure(state, namespace, repoName) ||
|
||||
getModifyPermissionsFailure(state, namespace, repoName);
|
||||
const loading = isFetchPermissionsPending(state, namespace, repoName);
|
||||
getModifyPermissionsFailure(state, namespace, repoName) ||
|
||||
getFetchAvailablePermissionsFailure(state);
|
||||
const loading =
|
||||
isFetchPermissionsPending(state, namespace, repoName) ||
|
||||
isFetchAvailablePermissionsPending(state);
|
||||
const permissions = getPermissionsOfRepo(state, namespace, repoName);
|
||||
const loadingCreatePermission = isCreatePermissionPending(
|
||||
state,
|
||||
@@ -177,7 +215,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const permissionsLink = getPermissionsLink(state, namespace, repoName);
|
||||
const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
|
||||
const userAutoCompleteLink = getUserAutoCompleteLink(state);
|
||||
const availablePermissions = getAvailablePermissions(state);
|
||||
return {
|
||||
availablePermissions,
|
||||
namespace,
|
||||
repoName,
|
||||
error,
|
||||
@@ -196,6 +236,9 @@ const mapDispatchToProps = dispatch => {
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => {
|
||||
dispatch(fetchPermissions(link, namespace, repoName));
|
||||
},
|
||||
fetchAvailablePermissionsIfNeeded: () => {
|
||||
dispatch(fetchAvailablePermissionsIfNeeded());
|
||||
},
|
||||
createPermission: (
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { Permission } from "@scm-manager/ui-types";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
Permission
|
||||
} from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
modifyPermission,
|
||||
isModifyPermissionPending,
|
||||
deletePermission,
|
||||
isDeletePermissionPending
|
||||
isDeletePermissionPending,
|
||||
findMatchingRoleName
|
||||
} from "../modules/permissions";
|
||||
import { connect } from "react-redux";
|
||||
import type { History } from "history";
|
||||
import { Checkbox } from "@scm-manager/ui-components";
|
||||
import { Button, Checkbox } from "@scm-manager/ui-components";
|
||||
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
|
||||
import TypeSelector from "../components/TypeSelector";
|
||||
import RoleSelector from "../components/RoleSelector";
|
||||
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
|
||||
|
||||
type Props = {
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
submitForm: Permission => void,
|
||||
modifyPermission: (Permission, string, string) => void,
|
||||
modifyPermission: (permission: Permission, namespace: string, name: string) => void,
|
||||
permission: Permission,
|
||||
t: string => string,
|
||||
namespace: string,
|
||||
@@ -24,38 +30,53 @@ type Props = {
|
||||
match: any,
|
||||
history: History,
|
||||
loading: boolean,
|
||||
deletePermission: (Permission, string, string) => void,
|
||||
deletePermission: (permission: Permission, namespace: string, name: string) => void,
|
||||
deleteLoading: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
permission: Permission
|
||||
role: string,
|
||||
permission: Permission,
|
||||
showAdvancedDialog: boolean
|
||||
};
|
||||
|
||||
class SinglePermission extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const defaultPermission = props.availablePermissions.availableRoles
|
||||
? props.availablePermissions.availableRoles[0]
|
||||
: {};
|
||||
|
||||
this.state = {
|
||||
permission: {
|
||||
name: "",
|
||||
type: "READ",
|
||||
verbs: defaultPermission.verbs,
|
||||
groupPermission: false,
|
||||
_links: {}
|
||||
}
|
||||
},
|
||||
role: defaultPermission.name,
|
||||
showAdvancedDialog: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { permission } = this.props;
|
||||
const { availablePermissions, permission } = this.props;
|
||||
|
||||
const matchingRole = findMatchingRoleName(
|
||||
availablePermissions,
|
||||
permission.verbs
|
||||
);
|
||||
|
||||
if (permission) {
|
||||
this.setState({
|
||||
permission: {
|
||||
name: permission.name,
|
||||
type: permission.type,
|
||||
verbs: permission.verbs,
|
||||
groupPermission: permission.groupPermission,
|
||||
_links: permission._links
|
||||
}
|
||||
},
|
||||
role: matchingRole
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -69,28 +90,57 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { permission } = this.state;
|
||||
const { loading, namespace, repoName } = this.props;
|
||||
const typeSelector =
|
||||
this.props.permission._links && this.props.permission._links.update ? (
|
||||
const { role, permission, showAdvancedDialog } = this.state;
|
||||
const {
|
||||
t,
|
||||
availablePermissions,
|
||||
loading,
|
||||
namespace,
|
||||
repoName
|
||||
} = this.props;
|
||||
const availableRoleNames = availablePermissions.availableRoles.map(
|
||||
r => r.name
|
||||
);
|
||||
const readOnly = !this.mayChangePermissions();
|
||||
const roleSelector = readOnly ? (
|
||||
<td>{role}</td>
|
||||
) : (
|
||||
<td>
|
||||
<TypeSelector
|
||||
handleTypeChange={this.handleTypeChange}
|
||||
type={permission.type ? permission.type : "READ"}
|
||||
<RoleSelector
|
||||
handleRoleChange={this.handleRoleChange}
|
||||
availableRoles={availableRoleNames}
|
||||
role={role}
|
||||
loading={loading}
|
||||
/>
|
||||
</td>
|
||||
) : (
|
||||
<td>{permission.type}</td>
|
||||
);
|
||||
|
||||
const advancedDialg = showAdvancedDialog ? (
|
||||
<AdvancedPermissionsDialog
|
||||
readOnly={readOnly}
|
||||
availableVerbs={availablePermissions.availableVerbs}
|
||||
selectedVerbs={permission.verbs}
|
||||
onClose={this.closeAdvancedPermissionsDialog}
|
||||
onSubmit={this.submitAdvancedPermissionsDialog}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{permission.name}</td>
|
||||
<td>
|
||||
<Checkbox checked={permission ? permission.groupPermission : false} />
|
||||
<Checkbox
|
||||
checked={permission ? permission.groupPermission : false}
|
||||
disabled={true}
|
||||
/>
|
||||
</td>
|
||||
{roleSelector}
|
||||
<td>
|
||||
<Button
|
||||
label={t("permission.advanced-button.label")}
|
||||
action={this.handleDetailedPermissionsPressed}
|
||||
/>
|
||||
</td>
|
||||
{typeSelector}
|
||||
<td>
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
@@ -99,39 +149,69 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
deletePermission={this.deletePermission}
|
||||
loading={this.props.deleteLoading}
|
||||
/>
|
||||
{advancedDialg}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
handleTypeChange = (type: string) => {
|
||||
this.setState({
|
||||
permission: {
|
||||
...this.state.permission,
|
||||
type: type
|
||||
}
|
||||
});
|
||||
this.modifyPermission(type);
|
||||
mayChangePermissions = () => {
|
||||
return this.props.permission._links && this.props.permission._links.update;
|
||||
};
|
||||
|
||||
modifyPermission = (type: string) => {
|
||||
handleDetailedPermissionsPressed = () => {
|
||||
this.setState({ showAdvancedDialog: true });
|
||||
};
|
||||
|
||||
closeAdvancedPermissionsDialog = () => {
|
||||
this.setState({ showAdvancedDialog: false });
|
||||
};
|
||||
|
||||
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
|
||||
const { permission } = this.state;
|
||||
const newRole = findMatchingRoleName(
|
||||
this.props.availablePermissions,
|
||||
newVerbs
|
||||
);
|
||||
this.setState(
|
||||
{
|
||||
showAdvancedDialog: false,
|
||||
permission: { ...permission, verbs: newVerbs },
|
||||
role: newRole
|
||||
},
|
||||
() => this.modifyPermission(newVerbs)
|
||||
);
|
||||
};
|
||||
|
||||
handleRoleChange = (role: string) => {
|
||||
const selectedRole = this.findAvailableRole(role);
|
||||
this.setState(
|
||||
{
|
||||
permission: {
|
||||
...this.state.permission,
|
||||
verbs: selectedRole.verbs
|
||||
},
|
||||
role: role
|
||||
},
|
||||
() => this.modifyPermission(selectedRole.verbs)
|
||||
);
|
||||
};
|
||||
|
||||
findAvailableRole = (roleName: string) => {
|
||||
return this.props.availablePermissions.availableRoles.find(
|
||||
role => role.name === roleName
|
||||
);
|
||||
};
|
||||
|
||||
modifyPermission = (verbs: string[]) => {
|
||||
let permission = this.state.permission;
|
||||
permission.type = type;
|
||||
permission.verbs = verbs;
|
||||
this.props.modifyPermission(
|
||||
permission,
|
||||
this.props.namespace,
|
||||
this.props.repoName
|
||||
);
|
||||
};
|
||||
|
||||
createSelectOptions(types: string[]) {
|
||||
return types.map(type => {
|
||||
return {
|
||||
label: type,
|
||||
value: type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Action } from "@scm-manager/ui-components";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import * as types from "../../../modules/types";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
Permission,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry
|
||||
@@ -11,7 +12,18 @@ import type {
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
import { Dispatch } from "redux";
|
||||
import { getLinks } from "../../../modules/indexResource";
|
||||
|
||||
export const FETCH_AVAILABLE = "scm/permissions/FETCH_AVAILABLE";
|
||||
export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${
|
||||
types.PENDING_SUFFIX
|
||||
}`;
|
||||
export const FETCH_AVAILABLE_SUCCESS = `${FETCH_AVAILABLE}_${
|
||||
types.SUCCESS_SUFFIX
|
||||
}`;
|
||||
export const FETCH_AVAILABLE_FAILURE = `${FETCH_AVAILABLE}_${
|
||||
types.FAILURE_SUFFIX
|
||||
}`;
|
||||
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
|
||||
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
|
||||
types.PENDING_SUFFIX
|
||||
@@ -62,7 +74,71 @@ export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
|
||||
types.RESET_SUFFIX
|
||||
}`;
|
||||
|
||||
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
|
||||
const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json";
|
||||
|
||||
// fetch available permissions
|
||||
|
||||
export function fetchAvailablePermissionsIfNeeded() {
|
||||
return function(dispatch: any, getState: () => Object) {
|
||||
if (shouldFetchAvailablePermissions(getState())) {
|
||||
return fetchAvailablePermissions(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailablePermissions(
|
||||
dispatch: any,
|
||||
getState: () => Object
|
||||
) {
|
||||
dispatch(fetchAvailablePending());
|
||||
return apiClient
|
||||
.get(getLinks(getState()).availableRepositoryPermissions.href)
|
||||
.then(response => response.json())
|
||||
.then(available => {
|
||||
dispatch(fetchAvailableSuccess(available));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchAvailableFailure(err));
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldFetchAvailablePermissions(state: Object) {
|
||||
if (
|
||||
isFetchAvailablePermissionsPending(state) ||
|
||||
getFetchAvailablePermissionsFailure(state)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !state.available;
|
||||
}
|
||||
|
||||
export function fetchAvailablePending(): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_PENDING,
|
||||
payload: {},
|
||||
itemId: "available"
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailableSuccess(
|
||||
available: AvailableRepositoryPermissions
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_SUCCESS,
|
||||
payload: available,
|
||||
itemId: "available"
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailableFailure(error: Error): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_FAILURE,
|
||||
payload: {
|
||||
error
|
||||
},
|
||||
itemId: "available"
|
||||
};
|
||||
}
|
||||
|
||||
// fetch permissions
|
||||
|
||||
@@ -368,6 +444,7 @@ export function deletePermissionReset(namespace: string, repoName: string) {
|
||||
itemId: namespace + "/" + repoName
|
||||
};
|
||||
}
|
||||
|
||||
function deletePermissionFromState(
|
||||
oldPermissions: PermissionCollection,
|
||||
permission: Permission
|
||||
@@ -399,12 +476,17 @@ export default function reducer(
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case FETCH_AVAILABLE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
available: action.payload
|
||||
};
|
||||
case FETCH_PERMISSIONS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
[action.itemId]: {
|
||||
entries: action.payload._embedded.permissions,
|
||||
createPermission: action.payload._links.create ? true : false
|
||||
createPermission: !!action.payload._links.create
|
||||
}
|
||||
};
|
||||
case MODIFY_PERMISSION_SUCCESS:
|
||||
@@ -452,6 +534,12 @@ export default function reducer(
|
||||
|
||||
// selectors
|
||||
|
||||
export function getAvailablePermissions(state: Object) {
|
||||
if (state.permissions) {
|
||||
return state.permissions.available;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPermissionsOfRepo(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -463,6 +551,10 @@ export function getPermissionsOfRepo(
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchAvailablePermissionsPending(state: Object) {
|
||||
return isPending(state, FETCH_AVAILABLE, "available");
|
||||
}
|
||||
|
||||
export function isFetchPermissionsPending(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -471,6 +563,10 @@ export function isFetchPermissionsPending(
|
||||
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
|
||||
}
|
||||
|
||||
export function getFetchAvailablePermissionsFailure(state: Object) {
|
||||
return getFailure(state, FETCH_AVAILABLE, "available");
|
||||
}
|
||||
|
||||
export function getFetchPermissionsFailure(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -522,6 +618,7 @@ export function isCreatePermissionPending(
|
||||
) {
|
||||
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
|
||||
}
|
||||
|
||||
export function getCreatePermissionFailure(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -603,3 +700,33 @@ export function getModifyPermissionsFailure(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findMatchingRoleName(
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
verbs: string[]
|
||||
) {
|
||||
if (!verbs) {
|
||||
return "";
|
||||
}
|
||||
const matchingRole = availablePermissions.availableRoles.find(role => {
|
||||
return equalVerbs(role.verbs, verbs);
|
||||
});
|
||||
|
||||
if (matchingRole) {
|
||||
return matchingRole.name;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function equalVerbs(verbs1: string[], verbs2: string[]) {
|
||||
if (!verbs1 || !verbs2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (verbs1.length !== verbs2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return verbs1.every(verb => verbs2.includes(verb));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ const hitchhiker_puzzle42Permission_user_eins: Permission = {
|
||||
href:
|
||||
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
|
||||
}
|
||||
}
|
||||
},
|
||||
verbs: []
|
||||
};
|
||||
|
||||
const hitchhiker_puzzle42Permission_user_zwei: Permission = {
|
||||
@@ -79,7 +80,8 @@ const hitchhiker_puzzle42Permission_user_zwei: Permission = {
|
||||
href:
|
||||
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
|
||||
}
|
||||
}
|
||||
},
|
||||
verbs: []
|
||||
};
|
||||
|
||||
const hitchhiker_puzzle42Permissions: PermissionCollection = [
|
||||
@@ -175,8 +177,7 @@ describe("permission fetch", () => {
|
||||
}
|
||||
);
|
||||
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
editedPermission.type = "OWNER";
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -197,8 +198,7 @@ describe("permission fetch", () => {
|
||||
}
|
||||
);
|
||||
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
editedPermission.type = "OWNER";
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -227,8 +227,7 @@ describe("permission fetch", () => {
|
||||
}
|
||||
);
|
||||
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
editedPermission.type = "OWNER";
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -451,8 +450,7 @@ describe("permissions reducer", () => {
|
||||
entries: [hitchhiker_puzzle42Permission_user_eins]
|
||||
}
|
||||
};
|
||||
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
permissionEdited.type = "OWNER";
|
||||
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
let expectedState = {
|
||||
"hitchhiker/puzzle42": {
|
||||
entries: [permissionEdited]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Button } from "@scm-manager/ui-components";
|
||||
import { ButtonGroup } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
showHistory: boolean => void
|
||||
};
|
||||
|
||||
class ButtonGroup extends React.Component<Props> {
|
||||
class FileButtonGroup extends React.Component<Props> {
|
||||
showHistory = () => {
|
||||
this.props.showHistory(true);
|
||||
};
|
||||
@@ -21,15 +21,6 @@ class ButtonGroup extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, historyIsSelected } = this.props;
|
||||
|
||||
let sourcesColor = "";
|
||||
let historyColor = "";
|
||||
|
||||
if (historyIsSelected) {
|
||||
historyColor = "link is-selected";
|
||||
} else {
|
||||
sourcesColor = "link is-selected";
|
||||
}
|
||||
|
||||
const sourcesLabel = (
|
||||
<>
|
||||
<span className="icon">
|
||||
@@ -53,20 +44,15 @@ class ButtonGroup extends React.Component<Props> {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="buttons has-addons">
|
||||
<Button
|
||||
label={sourcesLabel}
|
||||
color={sourcesColor}
|
||||
action={this.showSources}
|
||||
<ButtonGroup
|
||||
firstlabel={sourcesLabel}
|
||||
secondlabel={historyLabel}
|
||||
firstAction={this.showSources}
|
||||
secondAction={this.showHistory}
|
||||
firstIsSelected={!historyIsSelected}
|
||||
/>
|
||||
<Button
|
||||
label={historyLabel}
|
||||
color={historyColor}
|
||||
action={this.showHistory}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(ButtonGroup);
|
||||
export default translate("repos")(FileButtonGroup);
|
||||
@@ -6,7 +6,7 @@ import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import FileSize from "../components/FileSize";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
import ButtonGroup from "../components/content/ButtonGroup";
|
||||
import FileButtonGroup from "../components/content/FileButtonGroup";
|
||||
import SourcesView from "./SourcesView";
|
||||
import HistoryView from "./HistoryView";
|
||||
import { getSources } from "../modules/sources";
|
||||
@@ -76,7 +76,7 @@ class Content extends React.Component<Props, State> {
|
||||
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
|
||||
|
||||
const selector = file._links.history ? (
|
||||
<ButtonGroup
|
||||
<FileButtonGroup
|
||||
file={file}
|
||||
historyIsSelected={showHistory}
|
||||
showHistory={(changeShowHistory: boolean) =>
|
||||
|
||||
@@ -81,7 +81,16 @@ $fa-font-path: "webfonts";
|
||||
height: 2.5rem;
|
||||
|
||||
&.is-primary {
|
||||
background-color: $mint;
|
||||
background-color: #00d1df;
|
||||
}
|
||||
&.is-primary:hover, &.is-primary.is-hovered {
|
||||
background-color: #00b9c6;
|
||||
}
|
||||
&.is-primary:active, &.is-primary.is-active {
|
||||
background-color: #00a1ac;
|
||||
}
|
||||
&.is-primary[disabled] {
|
||||
background-color: #40dde7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +133,6 @@ $fa-font-path: "webfonts";
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.overlay-half-column{
|
||||
position: absolute;
|
||||
height: calc(120px - 0.5rem);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user