diff --git a/docs/mercurial/clone-empty.md b/docs/mercurial/clone-empty.md new file mode 100644 index 0000000000..44a81de20c --- /dev/null +++ b/docs/mercurial/clone-empty.md @@ -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 +``` diff --git a/docs/mercurial/push-bookmark.md b/docs/mercurial/push-bookmark.md new file mode 100644 index 0000000000..9ed591f9f4 --- /dev/null +++ b/docs/mercurial/push-bookmark.md @@ -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 +``` diff --git a/docs/mercurial/push-multiple-branches-to-new.md b/docs/mercurial/push-multiple-branches-to-new.md new file mode 100644 index 0000000000..734c479fef --- /dev/null +++ b/docs/mercurial/push-multiple-branches-to-new.md @@ -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.~?.?..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..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 +``` diff --git a/docs/mercurial/push-multiple-branches.md b/docs/mercurial/push-multiple-branches.md new file mode 100644 index 0000000000..5827cb0ceb --- /dev/null +++ b/docs/mercurial/push-multiple-branches.md @@ -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 +``` diff --git a/docs/mercurial/push-single-changeset.md b/docs/mercurial/push-single-changeset.md new file mode 100644 index 0000000000..499b4c21c3 --- /dev/null +++ b/docs/mercurial/push-single-changeset.md @@ -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 +``` diff --git a/pom.xml b/pom.xml index 17e8ae2cca..2d8bc13f28 100644 --- a/pom.xml +++ b/pom.xml @@ -86,20 +86,6 @@ http://maven.scm-manager.org/nexus/content/groups/public - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - true - daily - - - - - jitpack - https://jitpack.io - - @@ -174,18 +160,6 @@ assertj-core - - - com.github.cloudogu - ces-build-lib - - 9aadeeb - - true - - provided - - @@ -376,6 +350,70 @@ 3.10.0 test + + + + + + commons-beanutils + commons-beanutils + 1.9.3 + + + + commons-collections + commons-collections + 3.2.2 + + + + + + org.apache.httpcomponents + httpclient + 4.5.5 + + + + + + slf4j-api + org.slf4j + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + + javax.xml.bind + jaxb-api + ${jaxb.version} + + + + com.sun.xml.bind + jaxb-impl + ${jaxb.version} + + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb.version} + + + + javax.activation + activation + 1.1.1 + + @@ -418,10 +456,11 @@ maven-surefire-plugin 2.22.0 + org.apache.maven.plugins maven-enforcer-plugin - 1.4.1 + 3.0.0-M1 enforce-java @@ -443,11 +482,31 @@ [3.1,) + + + 1.8 + + + module-info + + true + + + org.codehaus.mojo + extra-enforcer-rules + 1.0-beta-7 + + @@ -636,53 +695,53 @@ org.apache.maven.plugins maven-site-plugin - 3.2 - - - - - org.apache.maven.plugins - maven-project-info-reports-plugin - 2.4 - - - - org.apache.maven.plugins - maven-jxr-plugin - 2.3 - - - - org.codehaus.mojo - findbugs-maven-plugin - 2.4.0 - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.12 - - - - org.apache.maven.plugins - maven-pmd-plugin - 2.7.1 - - true - ${project.build.sourceEncoding} - ${project.build.javaLevel} - - - - - + 3.7 + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.4 + + + + org.apache.maven.plugins + maven-jxr-plugin + 2.3 + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.4.0 + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.12 + + + + org.apache.maven.plugins + maven-pmd-plugin + 2.7.1 + + ${project.build.sourceEncoding} + ${project.build.javaLevel} + + + + + + @@ -726,17 +785,9 @@ org.apache.maven.plugins maven-javadoc-plugin + 3.0.0 - org.jboss.apiviz.APIviz - - org.jboss.apiviz - apiviz - 1.3.2.GA - - - -sourceclasspath ${project.build.outputDirectory} - -nopackagediagram - + false @@ -770,8 +821,8 @@ 5.2.0 - 1.7.22 - 1.1.10 + 1.7.25 + 1.2.3 3.0.1 2.0.1 @@ -780,21 +831,22 @@ 2.11.1 2.8.6 4.0 + 2.3.0 1.4.2 - 9.2.10.v20150310 - 9.2.10.v20150310 + 9.4.14.v20181114 + 9.4.14.v20181114 1.1.0 1.4.0 - - v4.5.2.201704071617-r-scm1 - 1.8.15-scm1 + + v4.5.3.201708160445-r-scm1 + 1.9.0-scm3 26.0-jre @@ -806,6 +858,7 @@ 1.8 + 1.8 UTF-8 SCM-BSD diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java new file mode 100644 index 0000000000..680dd25563 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java @@ -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); + } + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java new file mode 100644 index 0000000000..2321b3d9d9 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java @@ -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; +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java new file mode 100644 index 0000000000..0f88ef662a --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java @@ -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); + } + } + +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java new file mode 100644 index 0000000000..123655a7e3 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java @@ -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(); + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java new file mode 100644 index 0000000000..d3bd6847a8 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java @@ -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); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java similarity index 59% rename from scm-core/src/main/java/sonia/scm/repository/PermissionType.java rename to scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java index bba0d44f3d..be600125b7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java @@ -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(); } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java new file mode 100644 index 0000000000..19ae135461 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java @@ -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); + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java new file mode 100644 index 0000000000..175d1986c6 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java @@ -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 Issue 978 + * @see Issue 979 + */ +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); + } +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java new file mode 100644 index 0000000000..14000faa33 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java @@ -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)); + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java new file mode 100644 index 0000000000..20422df7ed --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java @@ -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); + } + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java new file mode 100644 index 0000000000..d9edc64884 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java @@ -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; + } + +} diff --git a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java similarity index 55% rename from scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java rename to scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java index e8180ca24a..c9190c3357 100644 --- a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java @@ -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()); } + } diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java new file mode 100644 index 0000000000..4510a7a072 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java @@ -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; + } +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java new file mode 100644 index 0000000000..9f65d34655 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java @@ -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)); + } +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java new file mode 100644 index 0000000000..e847e89cb1 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java @@ -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()); + } + +} diff --git a/scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml b/scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml new file mode 100644 index 0000000000..94132772a4 Binary files /dev/null and b/scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml differ diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 66859a12ee..c4bf3a2e6f 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -24,9 +24,9 @@ ${servlet.version} provided - + - + sonia.scm scm-annotations @@ -39,7 +39,7 @@ provided - + @@ -47,6 +47,7 @@ org.slf4j ${slf4j.version} + jcl-over-slf4j org.slf4j @@ -80,7 +81,7 @@ guice-servlet ${guice.version} - + com.google.inject.extensions guice-throwingproviders @@ -136,29 +137,51 @@ - + com.github.legman core ${legman.version} - + + + + + javax.xml.bind + jaxb-api + + + + com.sun.xml.bind + jaxb-impl + + + + org.glassfish.jaxb + jaxb-runtime + + + + javax.activation + activation + + - + com.google.guava guava ${guava.version} - + commons-lang commons-lang 2.6 - + - + sonia.scm scm-annotation-processor @@ -186,13 +209,14 @@ - + - + org.apache.maven.plugins maven-javadoc-plugin + 3.0.0 true ${project.build.sourceEncoding} @@ -216,9 +240,23 @@ http://www.slf4j.org/api/ http://shiro.apache.org/static/${shiro.version}/apidocs/ + org.jboss.apiviz.APIviz + + org.jboss.apiviz + apiviz + 1.3.2.GA + + + + -sourceclasspath ${project.build.outputDirectory} + + + -nopackagediagram + + - + diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 622eed6ad6..568b75e525 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -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 permissions = new HashSet<>(); @XmlElement(name = "public") private boolean publicReadable = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java index 63d499deb8..737374025d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java @@ -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; diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java index 0aff771fce..9e132ef93c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -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 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 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 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 verbs) { - this.type = type; + this.verbs = verbs; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java index 6c7c620fa4..6098bdf92b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java @@ -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); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java index 2753128eac..d39c95e0e2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java @@ -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); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java index a0f5ff4115..969ec6ef11 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java @@ -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(); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java index 7b318e49ec..a734225281 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java @@ -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()); diff --git a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java deleted file mode 100644 index 1b0229d6f5..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java +++ /dev/null @@ -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; -} diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 8596bab754..19859b876b 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -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; diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index 328494a626..a062fdb360 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator } else { - permitted = RepositoryPermissions.read(repository).isPermitted(); + permitted = RepositoryPermissions.pull(repository).isPermitted(); } return permitted; diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java new file mode 100644 index 0000000000..2e9383b2e2 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java @@ -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); + } +} diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini index fbdd35ba50..fda268ec83 100644 --- a/scm-core/src/test/resources/sonia/scm/shiro.ini +++ b/scm-core/src/test/resources/sonia/scm/shiro.ini @@ -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" diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 6330db56a0..aebdf010e2 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -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", "read", "write"); + assertThat(content).containsSubsequence("vogons", "delete"); + } + @Test void shouldReadPathDatabaseAndMetadataOfRepositories() { Repository heartOfGold = createHeartOfGold(); diff --git a/scm-it/pom.xml b/scm-it/pom.xml index 3f518dd9fe..c38c73ce3e 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -11,7 +11,8 @@ sonia.scm scm-it - jar + + war 2.0.0-SNAPSHOT scm-it @@ -91,6 +92,14 @@ + + org.apache.maven.plugins + maven-war-plugin + + false + + + com.mycila.maven-license-plugin maven-license-plugin diff --git a/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java b/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java new file mode 100644 index 0000000000..1490a917df --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java @@ -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 results = push(force); + assertStatus(results, expectedStatus); + } + + private Iterable push(boolean force) throws GitAPIException { + return git.push() + .setRemote("origin") + .add("master") + .setForce(force) + .setCredentialsProvider(createCredentialProvider()) + .call(); + } + + private void assertStatus(Iterable 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); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index aa91e67022..926be5459f 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -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() diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 48605437c6..584737221f 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -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 READ = asList("read", "pull"); + public static final Collection WRITE = asList("read", "write", "pull", "push"); + public static final Collection OWNER = asList("*"); + private static final List PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static Map 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 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() diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index 25f85fc170..02085bb7c5 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -82,7 +82,6 @@ test - diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java index 3d07a91741..08602e2af1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java @@ -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) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index 03f38b0086..8f4de9aa30 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -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() { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java index e7a75a0ff4..bcc2dc8a18 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/api/GitHookTagProvider.java @@ -68,17 +68,40 @@ 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)); } } createdTags = createdTagBuilder.build(); deletedTags = deletedTagBuilder.build(); } - + + private Tag createTagFromNewId(ReceiveCommand rc, String tag) { + return new Tag(tag, GitUtil.getId(rc.getNewId())); + } + + private Tag createTagFromOldId(ReceiveCommand rc, String tag) { + return new Tag(tag, GitUtil.getId(rc.getOldId())); + } + + private boolean isUpdate(ReceiveCommand rc) { + return rc.getType() == ReceiveCommand.Type.UPDATE || rc.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD; + } + + private boolean isDelete(ReceiveCommand rc) { + return rc.getType() == ReceiveCommand.Type.DELETE; + } + + private boolean isCreate(ReceiveCommand rc) { + return rc.getType() == ReceiveCommand.Type.CREATE; + } + @Override public List getCreatedTags() { return createdTags; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java index 25bbe04cfc..5cb8007986 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceivePackFactory.java @@ -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 +public class GitReceivePackFactory implements ReceivePackFactory { - /** - * 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; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js index be977c53f3..363779c92b 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js +++ b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.js @@ -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 { }; 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 { onChange={this.handleChange} disabled={readOnly} /> + 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 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); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java new file mode 100644 index 0000000000..dc0822deba --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitReceivePackFactoryTest.java @@ -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()); + } + +} diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 6b7664c140..b07ce5bb3c 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -20,7 +20,7 @@ com.aragost.javahg javahg - 0.8-scm1 + 0.13-java7 com.google.guava @@ -81,7 +81,6 @@ - @@ -93,19 +92,6 @@ http://maven.scm-manager.org/nexus/content/groups/public - - - false - - - true - - sonatype-ossrh - Sonatype Open Source Software Repository Hosting - default - https://oss.sonatype.org/content/groups/public/ - - diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java index 5953b7c789..641b8650fc 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java @@ -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 diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java index 41b0f8d205..bb3d2b1cfb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java @@ -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 Issue 959 + */ + private boolean disableHookSSLValidation = false; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index 4119c3396c..45eba94442 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -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,11 +277,22 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet directory.getAbsolutePath()); // add hook environment + Map 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, + hookManager, request ); //J+ diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index 7f92cc0357..93b9699fc9 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -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 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; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java index 90f53a1fea..479c3bd986 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java @@ -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); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java new file mode 100644 index 0000000000..b0b2f8ef0d --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletInputStream.java @@ -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(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java new file mode 100644 index 0000000000..80251c140a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletRequest.java @@ -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; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java new file mode 100644 index 0000000000..fb84692805 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/WireProtocol.java @@ -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 Mercurial Wire Protocol + */ +public final class WireProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(WireProtocol.class); + + private static final Set 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 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 commands = commandsOf(request); + boolean write = isWriteRequest(commands); + LOG.trace("mercurial request {} is write: {}", commands, write); + return write; + } + + @VisibleForTesting + static boolean isWriteRequest(List commands) { + return !READ_COMMANDS.containsAll(commands); + } + + @VisibleForTesting + static List commandsOf(HttpServletRequest request) { + List 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 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 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 listOfCmds) { + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String header = (String) headerNames.nextElement(); + parseHgArgHeader(request, listOfCmds, header); + } + } + + private static void parseHgArgHeader(HttpServletRequest request, List listOfCmds, String header) { + if (isHgArgHeader(header)) { + String value = getHeaderDecoded(request, header); + parseHgArgValue(listOfCmds, value); + } + } + + private static void parseHgArgValue(List listOfCmds, String value) { + if (isHgArgCommandHeader(value)) { + parseHgCommandHeader(listOfCmds, value); + } + } + + private static void parseHgCommandHeader(List 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 queryParameterMap = createQueryParameterMap(request); + + Collection cmd = queryParameterMap.get("cmd"); + Preconditions.checkArgument(cmd.size() <= 1, "found more than one cmd query parameter"); + Iterator iterator = cmd.iterator(); + + String command = null; + if (iterator.hasNext()) { + command = iterator.next(); + } + return command; + } + + private static Multimap createQueryParameterMap(HttpServletRequest request) { + Multimap 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; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js index 8a96ea1801..8e370a70c6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js @@ -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 { {this.inputField("encoding")} {this.checkbox("useOptimizedBytecode")} {this.checkbox("showRevisionInId")} + {this.checkbox("disableHookSSLValidation")} + {this.checkbox("enableHttpPostArgs")} {this.checkbox("disabled")} ); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index 48f3bd57d6..ee1b6c8911 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -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" diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 6aa9bac2f8..c123660a83 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -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 = '' @@ -166,7 +181,7 @@ def collect_sub_repositories(revCtx): subrepos[parts[0].strip()] = subrepo except Exception: pass - + try: hgsubstate = revCtx.filectx('.hgsubstate').data().split('\n') for line in hgsubstate: @@ -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: diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py index a511800e9d..b14c8e8026 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/hgweb.py @@ -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) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py index 493016955d..c64a63abfa 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py @@ -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): diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style index 5c462fc459..2185c47a05 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/styles/changesets-eager.style @@ -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}" \ No newline at end of file diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java index f8aaedb32f..524e33e265 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java @@ -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; } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java index 5608bea6e8..b3a4a0c2a4 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgPermissionFilterTest.java @@ -31,21 +31,32 @@ 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}. - * + * * @author Sebastian Sdorra */ @RunWith(MockitoJUnitRunner.class) @@ -56,13 +67,37 @@ public class HgPermissionFilterTest { @Mock private ScmConfiguration configuration; - + @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)}. */ @@ -73,7 +108,7 @@ public class HgPermissionFilterTest { assertFalse(isWriteRequest("HEAD")); assertFalse(isWriteRequest("TRACE")); assertFalse(isWriteRequest("OPTIONS")); - + // write methods assertTrue(isWriteRequest("POST")); assertTrue(isWriteRequest("PUT")); @@ -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 Issue #970 + */ + @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)); + } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java new file mode 100644 index 0000000000..51b0a050fc --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgServletInputStreamTest.java @@ -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(); + } + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java new file mode 100644 index 0000000000..d1f5124b3a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolRequestMockFactory.java @@ -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 headers = Lists.newArrayList(); + + StringBuilder vary = new StringBuilder(); + for ( int i=0; i0) { + 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); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java new file mode 100644 index 0000000000..519dadfd6c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/WireProtocolTest.java @@ -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 cmdList = Lists.newArrayList(commands); + assertTrue(WireProtocol.isWriteRequest(cmdList)); + } + + private void assertIsReadRequest(String... commands) { + List 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 commands = WireProtocol.commandsOf(new HgServletRequest(request)); + assertThat(commands, contains("batch", "lheads", "known")); + } + + @Test + public void testGetCommandsOfWithBatch() { + prepareBatch("cmds=heads ;known nodes,ef5993bb4abb32a0565c347844c6d939fc4f4b98"); + List commands = WireProtocol.commandsOf(request); + assertThat(commands, contains("batch", "heads", "known")); + } + + @Test + public void testGetCommandsOfWithBatchEncoded() { + prepareBatch("cmds=heads+%3Bknown+nodes%3Def5993bb4abb32a0565c347844c6d939fc4f4b98"); + List 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 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 headers = Lists.newArrayList(); + for (int i=0; i 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(); + } + + } + +} diff --git a/scm-server/pom.xml b/scm-server/pom.xml index c8d004cec1..74f6466821 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -64,7 +64,6 @@ ${exploded.directory} lib flat - true @@ -306,8 +305,8 @@ - 1.0.15 - 1.0.15.1 + 1.1.0 + 1.1.0 ${project.build.directory}/appassembler/commons-daemon/scm-server diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 9aa9aa559c..bddbcf2c85 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -27,7 +27,7 @@ scm-core 2.0.0-SNAPSHOT - + com.github.sdorra shiro-unit diff --git a/scm-ui-components/packages/ui-components/src/BranchSelector.js b/scm-ui-components/packages/ui-components/src/BranchSelector.js index d03011bfdd..99c93fd677 100644 --- a/scm-ui-components/packages/ui-components/src/BranchSelector.js +++ b/scm-ui-components/packages/ui-components/src/BranchSelector.js @@ -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 { } 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 }); } diff --git a/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js b/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js new file mode 100644 index 0000000000..56ef66522a --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js @@ -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 { + + 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 ( +
+
+ ); + } +} + +export default ButtonGroup; diff --git a/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js b/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js index b39098d3a1..3df3e78680 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js +++ b/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js @@ -1,27 +1,28 @@ -//@flow -import React from "react"; -import injectSheet from "react-jss"; -import SubmitButton, { type ButtonProps } from "./SubmitButton"; -import classNames from "classnames"; - -const styles = { - spacing: { - marginTop: "2em", - border: "2px solid #e9f7fd", - padding: "1em 1em" - } - -}; - -class CreateButton extends React.Component { - render() { - const { classes } = this.props; - return ( -
- -
- ); - } -} - -export default injectSheet(styles)(CreateButton); +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import { type ButtonProps } from "./Button"; +import SubmitButton from "./SubmitButton"; +import classNames from "classnames"; + +const styles = { + spacing: { + marginTop: "2em", + border: "2px solid #e9f7fd", + padding: "1em 1em" + } + +}; + +class CreateButton extends React.Component { + render() { + const { classes } = this.props; + return ( +
+ +
+ ); + } +} + +export default injectSheet(styles)(CreateButton); diff --git a/scm-ui-components/packages/ui-components/src/buttons/index.js b/scm-ui-components/packages/ui-components/src/buttons/index.js index 2e166e1d93..014d92958d 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/index.js +++ b/scm-ui-components/packages/ui-components/src/buttons/index.js @@ -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"; diff --git a/scm-ui-components/packages/ui-components/src/forms/Select.js b/scm-ui-components/packages/ui-components/src/forms/Select.js index ccb82e62da..38e1cdab33 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Select.js +++ b/scm-ui-components/packages/ui-components/src/forms/Select.js @@ -54,7 +54,7 @@ class Select extends React.Component { > {options.map(opt => { return ( - ); diff --git a/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js new file mode 100644 index 0000000000..ab7e8d82e4 --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js @@ -0,0 +1,11 @@ +// @flow + +export type RepositoryRole = { + name: string, + verbs: string[] +}; + +export type AvailableRepositoryPermissions = { + availableVerbs: string[], + availableRoles: RepositoryRole[] +}; diff --git a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js index 4352c21da6..ed3c925283 100644 --- a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js +++ b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js @@ -7,7 +7,7 @@ export type Permission = PermissionCreateEntry & { export type PermissionCreateEntry = { name: string, - type: string, + verbs: string[], groupPermission: boolean } diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index cf739f747d..f7b375ac98 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -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"; diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json new file mode 100644 index 0000000000..4a4b7c89cf --- /dev/null +++ b/scm-ui/public/locales/de/commons.json @@ -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" + } +} diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json new file mode 100644 index 0000000000..0d8390a9bd --- /dev/null +++ b/scm-ui/public/locales/de/config.json @@ -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" + } +} diff --git a/scm-ui/public/locales/de/groups.json b/scm-ui/public/locales/de/groups.json new file mode 100644 index 0000000000..86528ee82a --- /dev/null +++ b/scm-ui/public/locales/de/groups.json @@ -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" + } +} diff --git a/scm-ui/public/locales/de/permissions.json b/scm-ui/public/locales/de/permissions.json new file mode 100644 index 0000000000..d1280808e5 --- /dev/null +++ b/scm-ui/public/locales/de/permissions.json @@ -0,0 +1,8 @@ +{ + "form": { + "submit-button": { + "label": "Berechtigungen speichern" + }, + "set-permissions-successful": "Berechtigungen erfolgreich gespeichert" + } +} diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json new file mode 100644 index 0000000000..038d5cdc54 --- /dev/null +++ b/scm-ui/public/locales/de/repos.json @@ -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." + } +} diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json new file mode 100644 index 0000000000..bb033e02dd --- /dev/null +++ b/scm-ui/public/locales/de/users.json @@ -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." + } +} diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index e3e1dbf032..6c3fa6628a 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -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" } } diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index d6cdaa1d8d..7eef6f79b1 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -87,44 +87,56 @@ "label": "Branches" }, "permission": { - "user": "User", - "group": "Group", - "error-title": "Error", - "error-subtitle": "Unknown permissions error", - "name": "User or Group", - "type": "Type", - "group-permission": "Group Permission", - "user-permission": "User Permission", - "edit-permission": { - "delete-button": "Delete", - "save-button": "Save Changes" - }, - "delete-permission-button": { - "label": "Delete", - "confirm-alert": { - "title": "Delete permission", - "message": "Do you really want to delete the permission?", - "submit": "Yes", - "cancel": "No" - } - }, - "add-permission": { - "add-permission-heading": "Add new Permission", - "submit-button": "Submit", - "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.", - "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" - }, - "autocomplete": { - "no-group-options": "No group suggestion available", - "group-placeholder": "Enter group", - "no-user-options": "No user suggestion available", - "user-placeholder": "Enter user", - "loading": "Loading..." + "user": "User", + "group": "Group", + "error-title": "Error", + "error-subtitle": "Unknown permissions error", + "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": { + "title": "Delete permission", + "message": "Do you really want to delete the permission?", + "submit": "Yes", + "cancel": "No" } + }, + "add-permission": { + "add-permission-heading": "Add new Permission", + "submit-button": "Submit", + "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. If this is not checked, it is a user permission.", + "nameHelpText": "Manage permissions for a specific user or group", + "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", + "group-placeholder": "Enter group", + "no-user-options": "No user suggestion available", + "user-placeholder": "Enter user", + "loading": "Loading..." + }, + "advanced": { + "dialog": { + "title": "Advanced permissions", + "submit": "Submit", + "abort": "Abort" + } + } }, "help": { "nameHelpText": "The name of the repository. This name will be part of the repository url.", diff --git a/scm-ui/src/repos/permissions/components/PermissionCheckbox.js b/scm-ui/src/repos/permissions/components/PermissionCheckbox.js new file mode 100644 index 0000000000..4ee6b6b768 --- /dev/null +++ b/scm-ui/src/repos/permissions/components/PermissionCheckbox.js @@ -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 { + render() { + const { t } = this.props; + return ( + + ); + } +} + +export default translate("plugins")(PermissionCheckbox); diff --git a/scm-ui/src/repos/permissions/components/RoleSelector.js b/scm-ui/src/repos/permissions/components/RoleSelector.js new file mode 100644 index 0000000000..d472f17c4b --- /dev/null +++ b/scm-ui/src/repos/permissions/components/RoleSelector.js @@ -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 { + render() { + const { + availableRoles, + role, + handleRoleChange, + loading, + label, + helpText + } = this.props; + + if (!availableRoles) return null; + + const options = role + ? this.createSelectOptions(availableRoles) + : ["", ...this.createSelectOptions(availableRoles)]; + + return ( + - ); - } - - createSelectOptions(types: string[]) { - return types.map(type => { - return { - label: type, - value: type - }; - }); - } -} - -export default translate("repos")(TypeSelector); diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.test.js b/scm-ui/src/repos/permissions/components/permissionValidation.test.js index b2e7e8be68..1149075537 100644 --- a/scm-ui/src/repos/permissions/components/permissionValidation.test.js +++ b/scm-ui/src/repos/permissions/components/permissionValidation.test.js @@ -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"; diff --git a/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js b/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js new file mode 100644 index 0000000000..0d844fdf3f --- /dev/null +++ b/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js @@ -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 { + 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 => ( + + )); + + const submitButton = !readOnly ? ( + + ) : null; + + return ( +
+
+
+
+

+ {t("permission.advanced.dialog.title")} +

+
+
+
{verbSelectBoxes}
+
+ {submitButton} +
+
+
+ ); + } + + 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); diff --git a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js similarity index 53% rename from scm-ui/src/repos/permissions/components/CreatePermissionForm.js rename to scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 488ed7ce2b..362b4aa48a 100644 --- a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -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 { @@ -33,10 +43,11 @@ class CreatePermissionForm extends React.Component { 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 { }; 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 ? ( + + ) : null; return (
@@ -131,32 +156,57 @@ class CreatePermissionForm extends React.Component {

{t("permission.add-permission.add-permission-heading")}

+ {advancedDialog}
- - +
+ + +
+
-
+
{this.renderAutocompletionField()}
-
- +
+
+
+ +
+
+ +
+
@@ -173,10 +223,25 @@ class CreatePermissionForm extends React.Component { ); } + 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 { 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); diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 48f4e585f7..7c20f503c5 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -1,225 +1,268 @@ -//@flow -import React from "react"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; -import { - fetchPermissions, - getFetchPermissionsFailure, - isFetchPermissionsPending, - getPermissionsOfRepo, - hasCreatePermission, - createPermission, - isCreatePermissionPending, - getCreatePermissionFailure, - createPermissionReset, - getDeletePermissionsFailure, - getModifyPermissionsFailure, - modifyPermissionReset, - deletePermissionReset -} from "../modules/permissions"; -import { Loading, ErrorPage } from "@scm-manager/ui-components"; -import type { - Permission, - PermissionCollection, - PermissionCreateEntry -} from "@scm-manager/ui-types"; -import SinglePermission from "./SinglePermission"; -import CreatePermissionForm from "../components/CreatePermissionForm"; -import type { History } from "history"; -import { getPermissionsLink } from "../../modules/repos"; -import { - getGroupAutoCompleteLink, - getUserAutoCompleteLink -} from "../../../modules/indexResource"; - -type Props = { - namespace: string, - repoName: string, - loading: boolean, - error: Error, - permissions: PermissionCollection, - hasPermissionToCreate: boolean, - loadingCreatePermission: boolean, - permissionsLink: string, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, - - //dispatch functions - fetchPermissions: (link: string, namespace: string, repoName: string) => void, - createPermission: ( - link: string, - permission: PermissionCreateEntry, - namespace: string, - repoName: string, - callback?: () => void - ) => void, - createPermissionReset: (string, string) => void, - modifyPermissionReset: (string, string) => void, - deletePermissionReset: (string, string) => void, - // context props - t: string => string, - match: any, - history: History -}; - -class Permissions extends React.Component { - componentDidMount() { - const { - fetchPermissions, - namespace, - repoName, - modifyPermissionReset, - createPermissionReset, - deletePermissionReset, - permissionsLink - } = this.props; - - createPermissionReset(namespace, repoName); - modifyPermissionReset(namespace, repoName); - deletePermissionReset(namespace, repoName); - fetchPermissions(permissionsLink, namespace, repoName); - } - - createPermission = (permission: Permission) => { - this.props.createPermission( - this.props.permissionsLink, - permission, - this.props.namespace, - this.props.repoName - ); - }; - - render() { - const { - loading, - error, - permissions, - t, - namespace, - repoName, - loadingCreatePermission, - hasPermissionToCreate, - userAutoCompleteLink, - groupAutoCompleteLink - } = this.props; - if (error) { - return ( - - ); - } - - if (loading || !permissions) { - return ; - } - - const createPermissionForm = hasPermissionToCreate ? ( - this.createPermission(permission)} - loading={loadingCreatePermission} - currentPermissions={permissions} - userAutoCompleteLink={userAutoCompleteLink} - groupAutoCompleteLink={groupAutoCompleteLink} - /> - ) : null; - - return ( -
- - - - - - - - - - {permissions.map(permission => { - return ( - - ); - })} - -
{t("permission.name")} - {t("permission.group-permission")} - {t("permission.type")} -
- {createPermissionForm} -
- ); - } -} - -const mapStateToProps = (state, ownProps) => { - const namespace = ownProps.namespace; - const repoName = ownProps.repoName; - const error = - getFetchPermissionsFailure(state, namespace, repoName) || - getCreatePermissionFailure(state, namespace, repoName) || - getDeletePermissionsFailure(state, namespace, repoName) || - getModifyPermissionsFailure(state, namespace, repoName); - const loading = isFetchPermissionsPending(state, namespace, repoName); - const permissions = getPermissionsOfRepo(state, namespace, repoName); - const loadingCreatePermission = isCreatePermissionPending( - state, - namespace, - repoName - ); - const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); - const permissionsLink = getPermissionsLink(state, namespace, repoName); - const groupAutoCompleteLink = getGroupAutoCompleteLink(state); - const userAutoCompleteLink = getUserAutoCompleteLink(state); - return { - namespace, - repoName, - error, - loading, - permissions, - hasPermissionToCreate, - loadingCreatePermission, - permissionsLink, - groupAutoCompleteLink, - userAutoCompleteLink - }; -}; - -const mapDispatchToProps = dispatch => { - return { - fetchPermissions: (link: string, namespace: string, repoName: string) => { - dispatch(fetchPermissions(link, namespace, repoName)); - }, - createPermission: ( - link: string, - permission: PermissionCreateEntry, - namespace: string, - repoName: string, - callback?: () => void - ) => { - dispatch( - createPermission(link, permission, namespace, repoName, callback) - ); - }, - createPermissionReset: (namespace: string, repoName: string) => { - dispatch(createPermissionReset(namespace, repoName)); - }, - modifyPermissionReset: (namespace: string, repoName: string) => { - dispatch(modifyPermissionReset(namespace, repoName)); - }, - deletePermissionReset: (namespace: string, repoName: string) => { - dispatch(deletePermissionReset(namespace, repoName)); - } - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("repos")(Permissions)); +//@flow +import React from "react"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { + fetchAvailablePermissionsIfNeeded, + fetchPermissions, + getFetchAvailablePermissionsFailure, + getAvailablePermissions, + getFetchPermissionsFailure, + isFetchAvailablePermissionsPending, + isFetchPermissionsPending, + getPermissionsOfRepo, + hasCreatePermission, + createPermission, + isCreatePermissionPending, + getCreatePermissionFailure, + createPermissionReset, + getDeletePermissionsFailure, + getModifyPermissionsFailure, + modifyPermissionReset, + deletePermissionReset +} from "../modules/permissions"; +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 "./CreatePermissionForm"; +import type { History } from "history"; +import { getPermissionsLink } from "../../modules/repos"; +import { + getGroupAutoCompleteLink, + getUserAutoCompleteLink +} from "../../../modules/indexResource"; + +type Props = { + availablePermissions: AvailableRepositoryPermissions, + namespace: string, + repoName: string, + loading: boolean, + error: Error, + permissions: PermissionCollection, + hasPermissionToCreate: boolean, + loadingCreatePermission: boolean, + permissionsLink: string, + groupAutoCompleteLink: string, + userAutoCompleteLink: string, + + //dispatch functions + fetchAvailablePermissionsIfNeeded: () => void, + fetchPermissions: (link: string, namespace: string, repoName: string) => void, + createPermission: ( + link: string, + permission: PermissionCreateEntry, + namespace: string, + repoName: string, + callback?: () => void + ) => void, + createPermissionReset: (string, string) => void, + modifyPermissionReset: (string, string) => void, + deletePermissionReset: (string, string) => void, + // context props + t: string => string, + match: any, + history: History +}; + +class Permissions extends React.Component { + componentDidMount() { + const { + fetchAvailablePermissionsIfNeeded, + fetchPermissions, + namespace, + repoName, + modifyPermissionReset, + createPermissionReset, + deletePermissionReset, + permissionsLink + } = this.props; + + createPermissionReset(namespace, repoName); + modifyPermissionReset(namespace, repoName); + deletePermissionReset(namespace, repoName); + fetchAvailablePermissionsIfNeeded(); + fetchPermissions(permissionsLink, namespace, repoName); + } + + createPermission = (permission: Permission) => { + this.props.createPermission( + this.props.permissionsLink, + permission, + this.props.namespace, + this.props.repoName + ); + }; + + render() { + const { + availablePermissions, + loading, + error, + permissions, + t, + namespace, + repoName, + loadingCreatePermission, + hasPermissionToCreate, + userAutoCompleteLink, + groupAutoCompleteLink + } = this.props; + if (error) { + return ( + + ); + } + + if (loading || !permissions || !availablePermissions) { + return ; + } + + const createPermissionForm = hasPermissionToCreate ? ( + this.createPermission(permission)} + loading={loadingCreatePermission} + currentPermissions={permissions} + userAutoCompleteLink={userAutoCompleteLink} + groupAutoCompleteLink={groupAutoCompleteLink} + /> + ) : null; + + return ( +
+ + + + + + + + + + + {permissions.map(permission => { + return ( + + ); + })} + +
+ + + + + + + + +
+ {createPermissionForm} +
+ ); + } +} + +const mapStateToProps = (state, ownProps) => { + const namespace = ownProps.namespace; + const repoName = ownProps.repoName; + const error = + getFetchPermissionsFailure(state, namespace, repoName) || + getCreatePermissionFailure(state, namespace, repoName) || + getDeletePermissionsFailure(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, + namespace, + repoName + ); + const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); + const permissionsLink = getPermissionsLink(state, namespace, repoName); + const groupAutoCompleteLink = getGroupAutoCompleteLink(state); + const userAutoCompleteLink = getUserAutoCompleteLink(state); + const availablePermissions = getAvailablePermissions(state); + return { + availablePermissions, + namespace, + repoName, + error, + loading, + permissions, + hasPermissionToCreate, + loadingCreatePermission, + permissionsLink, + groupAutoCompleteLink, + userAutoCompleteLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchPermissions: (link: string, namespace: string, repoName: string) => { + dispatch(fetchPermissions(link, namespace, repoName)); + }, + fetchAvailablePermissionsIfNeeded: () => { + dispatch(fetchAvailablePermissionsIfNeeded()); + }, + createPermission: ( + link: string, + permission: PermissionCreateEntry, + namespace: string, + repoName: string, + callback?: () => void + ) => { + dispatch( + createPermission(link, permission, namespace, repoName, callback) + ); + }, + createPermissionReset: (namespace: string, repoName: string) => { + dispatch(createPermissionReset(namespace, repoName)); + }, + modifyPermissionReset: (namespace: string, repoName: string) => { + dispatch(modifyPermissionReset(namespace, repoName)); + }, + deletePermissionReset: (namespace: string, repoName: string) => { + dispatch(deletePermissionReset(namespace, repoName)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("repos")(Permissions)); diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js index 62380128be..b16d26e903 100644 --- a/scm-ui/src/repos/permissions/containers/SinglePermission.js +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -1,176 +1,256 @@ -// @flow -import React from "react"; -import type { Permission } from "@scm-manager/ui-types"; -import { translate } from "react-i18next"; -import { - modifyPermission, - isModifyPermissionPending, - deletePermission, - isDeletePermissionPending -} from "../modules/permissions"; -import { connect } from "react-redux"; -import type { History } from "history"; -import { Checkbox } from "@scm-manager/ui-components"; -import DeletePermissionButton from "../components/buttons/DeletePermissionButton"; -import TypeSelector from "../components/TypeSelector"; - -type Props = { - submitForm: Permission => void, - modifyPermission: (Permission, string, string) => void, - permission: Permission, - t: string => string, - namespace: string, - repoName: string, - match: any, - history: History, - loading: boolean, - deletePermission: (Permission, string, string) => void, - deleteLoading: boolean -}; - -type State = { - permission: Permission -}; - -class SinglePermission extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - permission: { - name: "", - type: "READ", - groupPermission: false, - _links: {} - } - }; - } - - componentDidMount() { - const { permission } = this.props; - if (permission) { - this.setState({ - permission: { - name: permission.name, - type: permission.type, - groupPermission: permission.groupPermission, - _links: permission._links - } - }); - } - } - - deletePermission = () => { - this.props.deletePermission( - this.props.permission, - this.props.namespace, - this.props.repoName - ); - }; - - render() { - const { permission } = this.state; - const { loading, namespace, repoName } = this.props; - const typeSelector = - this.props.permission._links && this.props.permission._links.update ? ( - - - - ) : ( - {permission.type} - ); - - return ( - - {permission.name} - - - - {typeSelector} - - - - - ); - } - - handleTypeChange = (type: string) => { - this.setState({ - permission: { - ...this.state.permission, - type: type - } - }); - this.modifyPermission(type); - }; - - modifyPermission = (type: string) => { - let permission = this.state.permission; - permission.type = type; - 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) => { - const permission = ownProps.permission; - const loading = isModifyPermissionPending( - state, - ownProps.namespace, - ownProps.repoName, - permission - ); - const deleteLoading = isDeletePermissionPending( - state, - ownProps.namespace, - ownProps.repoName, - permission - ); - - return { loading, deleteLoading }; -}; - -const mapDispatchToProps = dispatch => { - return { - modifyPermission: ( - permission: Permission, - namespace: string, - repoName: string - ) => { - dispatch(modifyPermission(permission, namespace, repoName)); - }, - deletePermission: ( - permission: Permission, - namespace: string, - repoName: string - ) => { - dispatch(deletePermission(permission, namespace, repoName)); - } - }; -}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("repos")(SinglePermission)); +// @flow +import React from "react"; +import type { + AvailableRepositoryPermissions, + Permission +} from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { + modifyPermission, + isModifyPermissionPending, + deletePermission, + isDeletePermissionPending, + findMatchingRoleName +} from "../modules/permissions"; +import { connect } from "react-redux"; +import type { History } from "history"; +import { Button, Checkbox } from "@scm-manager/ui-components"; +import DeletePermissionButton from "../components/buttons/DeletePermissionButton"; +import RoleSelector from "../components/RoleSelector"; +import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; + +type Props = { + availablePermissions: AvailableRepositoryPermissions, + submitForm: Permission => void, + modifyPermission: (permission: Permission, namespace: string, name: string) => void, + permission: Permission, + t: string => string, + namespace: string, + repoName: string, + match: any, + history: History, + loading: boolean, + deletePermission: (permission: Permission, namespace: string, name: string) => void, + deleteLoading: boolean +}; + +type State = { + role: string, + permission: Permission, + showAdvancedDialog: boolean +}; + +class SinglePermission extends React.Component { + constructor(props: Props) { + super(props); + + const defaultPermission = props.availablePermissions.availableRoles + ? props.availablePermissions.availableRoles[0] + : {}; + + this.state = { + permission: { + name: "", + verbs: defaultPermission.verbs, + groupPermission: false, + _links: {} + }, + role: defaultPermission.name, + showAdvancedDialog: false + }; + } + + componentDidMount() { + const { availablePermissions, permission } = this.props; + + const matchingRole = findMatchingRoleName( + availablePermissions, + permission.verbs + ); + + if (permission) { + this.setState({ + permission: { + name: permission.name, + verbs: permission.verbs, + groupPermission: permission.groupPermission, + _links: permission._links + }, + role: matchingRole + }); + } + } + + deletePermission = () => { + this.props.deletePermission( + this.props.permission, + this.props.namespace, + this.props.repoName + ); + }; + + render() { + 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 ? ( + {role} + ) : ( + + + + ); + + const advancedDialg = showAdvancedDialog ? ( + + ) : null; + + return ( + + {permission.name} + + + + {roleSelector} + +
+ ); } } -export default translate("repos")(ButtonGroup); +export default translate("repos")(FileButtonGroup); diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index 6339c49a3d..a7e9874058 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -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 { const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; const selector = file._links.history ? ( - diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index d31f40ceea..be708d1bea 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -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); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 59add29fc4..c83cea36d5 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -18,14 +18,14 @@ - + sonia.scm scm-annotation-processor 2.0.0-SNAPSHOT provided - + javax.servlet javax.servlet-api @@ -69,17 +69,19 @@ shiro-guice ${shiro.version} - + io.jsonwebtoken jjwt-impl ${jjwt.version} + io.jsonwebtoken jjwt-api ${jjwt.version} + io.jsonwebtoken jjwt-jackson @@ -99,16 +101,19 @@ jackson-jaxrs-base ${jackson.version} + com.fasterxml.jackson.datatype jackson-datatype-jdk8 ${jackson.version} + com.fasterxml.jackson.datatype jackson-datatype-jsr310 ${jackson.version} + javax javaee-api @@ -146,11 +151,13 @@ org.jboss.resteasy resteasy-servlet-initializer + org.jboss.resteasy resteasy-validator-provider-11 ${resteasy.version} + @@ -160,19 +167,18 @@ - + com.github.legman.support shiro ${legman.version} - + ch.qos.logback logback-classic - ${logback.version} @@ -186,26 +192,34 @@ log4j-over-slf4j ${slf4j.version} - + + + + + xml-apis + xml-apis + 1.4.01 + + commons-beanutils commons-beanutils - 1.9.2 commons-collections commons-collections - 3.2.1 - - - + commons-codec commons-codec @@ -229,17 +243,9 @@ - - - - - org.apache.httpcomponents - httpclient - 4.2.6 - - + - + com.github.spullara.mustache.java compiler @@ -263,6 +269,7 @@ tika-core 1.18 + @@ -277,7 +284,7 @@ - + org.seleniumhq.selenium selenium-java @@ -298,7 +305,7 @@ 2.21 test - + com.sun.jersey jersey-client @@ -320,7 +327,7 @@ shiro-unit test - + sonia.scm.plugins scm-git-plugin @@ -328,14 +335,14 @@ tests test - + sonia.scm.plugins scm-git-plugin 2.0.0-SNAPSHOT test - + sonia.scm.plugins scm-hg-plugin @@ -343,14 +350,14 @@ tests test - + sonia.scm.plugins scm-hg-plugin 2.0.0-SNAPSHOT test - + sonia.scm.plugins scm-svn-plugin @@ -358,7 +365,7 @@ tests test - + sonia.scm.plugins scm-svn-plugin @@ -489,7 +496,7 @@ true - + sonia.maven change-env @@ -539,8 +546,8 @@ ${project.basedir}/src/main/conf/jetty.xml 0 - - + + scm-webapp @@ -554,6 +561,8 @@ 2.53.1 1.0 0.8.17 + 3.1.4.Final + 2.8.9 Tomcat e1 javascript:S3827 diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/Permission.java b/scm-webapp/src/main/java/sonia/scm/api/rest/Permission.java deleted file mode 100644 index ba707c19c2..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/Permission.java +++ /dev/null @@ -1,168 +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.api.rest; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import java.io.Serializable; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -public class Permission implements Serializable -{ - - /** Field description */ - private static final long serialVersionUID = 4320217034601679261L; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public Permission() {} - - /** - * Constructs ... - * - * - * @param id - * @param value - */ - public Permission(String id, String value) - { - this.id = id; - this.value = value; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param obj - * - * @return - */ - @Override - public boolean equals(Object obj) - { - if (obj == null) - { - return false; - } - - if (getClass() != obj.getClass()) - { - return false; - } - - final Permission other = (Permission) obj; - - return Objects.equal(id, other.id) && Objects.equal(value, other.value); - } - - /** - * Method description - * - * - * @return - */ - @Override - public int hashCode() - { - return Objects.hashCode(id, value); - } - - /** - * Method description - * - * - * @return - */ - @Override - public String toString() - { - //J- - return MoreObjects.toStringHelper(this) - .add("id", id) - .add("value", value) - .toString(); - //J+ - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getId() - { - return id; - } - - /** - * Method description - * - * - * @return - */ - public String getValue() - { - return value; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String id; - - /** Field description */ - private String value; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java index f00072cdcb..4253c456fb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java @@ -35,6 +35,8 @@ package sonia.scm.api.rest.resources; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.UrlEscapers; import org.apache.commons.beanutils.BeanComparator; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; @@ -45,7 +47,6 @@ import sonia.scm.ModelObject; import sonia.scm.PageResult; import sonia.scm.api.rest.RestExceptionResult; import sonia.scm.util.AssertUtil; -import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import javax.ws.rs.core.CacheControl; @@ -63,6 +64,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Date; +import java.net.URI; //~--- JDK imports ------------------------------------------------------------ @@ -139,11 +141,7 @@ public abstract class AbstractManagerResource { manager.create(item); String id = getId(item); - - id = HttpUtil.encode(id); - response = Response.created( - uriInfo.getAbsolutePath().resolve( - getPathPart().concat("/").concat(id))).build(); + response = Response.created(location(uriInfo, id)).build(); } catch (AuthorizationException ex) { @@ -159,6 +157,12 @@ public abstract class AbstractManagerResource { return response; } + @VisibleForTesting + URI location(UriInfo uriInfo, String id) { + String escaped = UrlEscapers.urlPathSegmentEscaper().escape(id); + return uriInfo.getAbsolutePath().resolve(getPathPart().concat("/").concat(escaped)); + } + /** * Method description * @@ -247,7 +251,7 @@ public abstract class AbstractManagerResource { */ public Response get(Request request, String id) { - Response response = null; + Response response; T item = manager.get(id); if (item != null) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java new file mode 100644 index 0000000000..60203b565b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailableRepositoryPermissionsDto.java @@ -0,0 +1,31 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import sonia.scm.security.RepositoryRole; + +import java.util.Collection; + +public class AvailableRepositoryPermissionsDto extends HalRepresentation { + private final Collection availableVerbs; + private final Collection availableRoles; + + public AvailableRepositoryPermissionsDto(Collection availableVerbs, Collection availableRoles) { + this.availableVerbs = availableVerbs; + this.availableRoles = availableRoles; + } + + public Collection getAvailableVerbs() { + return availableVerbs; + } + + public Collection getAvailableRoles() { + return availableRoles; + } + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 6377f21163..3eff661385 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -56,6 +56,7 @@ public class IndexDtoGenerator extends LinkAppenderMapper { if (PermissionPermissions.list().isPermitted()) { builder.single(link("permissions", resourceLinks.permissions().self())); } + builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self())); } else { builder.single(link("login", resourceLinks.authentication().jsonLogin())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 4cf66f7b28..859a6481f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -25,7 +25,7 @@ public class MapperModule extends AbstractModule { bind(RepositoryTypeCollectionToDtoMapper.class); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); - bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); + bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass()); bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 420e08fe96..a9bd5c2424 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -6,10 +6,9 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.SecurityUtils; -import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.user.User; import sonia.scm.web.VndMediaType; @@ -24,6 +23,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; public class RepositoryCollectionResource { @@ -100,7 +100,7 @@ public class RepositoryCollectionResource { private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { Repository repository = dtoToRepositoryMapper.map(repositoryDto, null); - repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), PermissionType.OWNER))); + repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), singletonList("*"), false))); return repository; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java index b7058d2830..b9add14529 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java @@ -10,7 +10,6 @@ public abstract class RepositoryDtoToRepositoryMapper extends BaseDtoMapper { @Mapping(target = "id", ignore = true) @Mapping(target = "publicReadable", ignore = true) @Mapping(target = "healthCheckFailures", ignore = true) - @Mapping(target = "permissions", ignore = true) public abstract Repository map(RepositoryDto repositoryDto, @Context String id); @AfterMapping diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index 6e6b9fd7fc..09683db488 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -7,9 +7,13 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; +import java.util.Collection; + import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN; @Getter @Setter @ToString @NoArgsConstructor @@ -20,16 +24,8 @@ public class RepositoryPermissionDto extends HalRepresentation { @Pattern(regexp = USER_GROUP_PATTERN) private String name; - /** - * the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable - * the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version - * - * see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19 - * - **/ - @JsonInclude(JsonInclude.Include.NON_NULL) - private String type; - + @NotEmpty + private Collection verbs; private boolean groupPermission = false; @@ -38,7 +34,6 @@ public class RepositoryPermissionDto extends HalRepresentation { this.groupPermission = groupPermission; } - @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package protected HalRepresentation add(Links links) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDtoToRepositoryPermissionMapper.java similarity index 74% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDtoToRepositoryPermissionMapper.java index 8d9761c28c..8e015e8b60 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDtoToRepositoryPermissionMapper.java @@ -1,11 +1,12 @@ package sonia.scm.api.v2.resources; +import org.mapstruct.CollectionMappingStrategy; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import sonia.scm.repository.RepositoryPermission; -@Mapper -public abstract class PermissionDtoToPermissionMapper { +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE) +public abstract class RepositoryPermissionDtoToRepositoryPermissionMapper { public abstract RepositoryPermission map(RepositoryPermissionDto permissionDto); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java new file mode 100644 index 0000000000..e5734085ca --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionResource.java @@ -0,0 +1,43 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import de.otto.edison.hal.Links; +import sonia.scm.security.RepositoryPermissionProvider; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +/** + * RESTful Web Service Resource to get available repository types. + */ +@Path(RepositoryPermissionResource.PATH) +public class RepositoryPermissionResource { + + static final String PATH = "v2/repositoryPermissions/"; + + private final RepositoryPermissionProvider repositoryPermissionProvider; + private final ResourceLinks resourceLinks; + + @Inject + public RepositoryPermissionResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) { + this.repositoryPermissionProvider = repositoryPermissionProvider; + this.resourceLinks = resourceLinks; + } + + @GET + @Path("") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.REPOSITORY_PERMISSION_COLLECTION) + public AvailableRepositoryPermissionsDto get() { + AvailableRepositoryPermissionsDto dto = new AvailableRepositoryPermissionsDto(repositoryPermissionProvider.availableVerbs(), repositoryPermissionProvider.availableRoles()); + dto.add(Links.linkingTo().self(resourceLinks.availableRepositoryPermissions().self()).build()); + return dto; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java similarity index 91% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java index cb3821cd67..dd66e6e5f2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResource.java @@ -35,18 +35,21 @@ import static sonia.scm.NotFoundException.notFound; import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; @Slf4j -public class PermissionRootResource { +public class RepositoryPermissionRootResource { - - private PermissionDtoToPermissionMapper dtoToModelMapper; + private RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper; private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper; private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; private ResourceLinks resourceLinks; private final RepositoryManager manager; - @Inject - public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper, RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + public RepositoryPermissionRootResource( + RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper, + RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper, + RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper, + ResourceLinks resourceLinks, + RepositoryManager manager) { this.dtoToModelMapper = dtoToModelMapper; this.modelToDtoMapper = modelToDtoMapper; this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper; @@ -54,7 +57,6 @@ public class PermissionRootResource { this.manager = manager; } - /** * Adds a new permission to the user or group managed by the repository * @@ -71,9 +73,9 @@ public class PermissionRootResource { @ResponseCode(code = 409, condition = "conflict") }) @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes(VndMediaType.PERMISSION) + @Consumes(VndMediaType.REPOSITORY_PERMISSION) @Path("") - public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid RepositoryPermissionDto permission) { + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryPermissionDto permission) { log.info("try to add new permission: {}", permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); @@ -84,7 +86,6 @@ public class PermissionRootResource { return Response.created(URI.create(resourceLinks.repositoryPermission().self(namespace, name, urlPermissionName))).build(); } - /** * Get the searched permission with permission name related to a repository * @@ -99,7 +100,7 @@ public class PermissionRootResource { @ResponseCode(code = 404, condition = "not found"), @ResponseCode(code = 500, condition = "internal server error") }) - @Produces(VndMediaType.PERMISSION) + @Produces(VndMediaType.REPOSITORY_PERMISSION) @TypeHint(RepositoryPermissionDto.class) @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) { @@ -115,7 +116,6 @@ public class PermissionRootResource { ).build(); } - /** * Get all permissions related to a repository * @@ -130,7 +130,7 @@ public class PermissionRootResource { @ResponseCode(code = 404, condition = "not found"), @ResponseCode(code = 500, condition = "internal server error") }) - @Produces(VndMediaType.PERMISSION) + @Produces(VndMediaType.REPOSITORY_PERMISSION) @TypeHint(RepositoryPermissionDto.class) @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) { @@ -139,7 +139,6 @@ public class PermissionRootResource { return Response.ok(repositoryPermissionCollectionToDtoMapper.map(repository)).build(); } - /** * Update a permission to the user or group managed by the repository * ignore the user input for groupPermission and take it from the path parameter (if the group prefix (@) exists it is a group permission) @@ -155,7 +154,7 @@ public class PermissionRootResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes(VndMediaType.PERMISSION) + @Consumes(VndMediaType.REPOSITORY_PERMISSION) @Path("{permission-name}") public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @@ -172,6 +171,7 @@ public class PermissionRootResource { if (!extractedPermissionName.equals(permission.getName())) { checkPermissionAlreadyExists(permission, repository); } + RepositoryPermission existingPermission = repository.getPermissions() .stream() .filter(filterPermission(permissionName)) @@ -208,17 +208,16 @@ public class PermissionRootResource { .stream() .filter(filterPermission(permissionName)) .findFirst() - .ifPresent(repository::removePermission) - ; + .ifPresent(repository::removePermission); manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); } - Predicate filterPermission(String permissionName) { - return permission -> getPermissionName(permissionName).equals(permission.getName()) + private Predicate filterPermission(String name) { + return permission -> getPermissionName(name).equals(permission.getName()) && - permission.isGroupPermission() == isGroupPermission(permissionName); + permission.isGroupPermission() == isGroupPermission(name); } private String getPermissionName(String permissionName) { @@ -231,7 +230,6 @@ public class PermissionRootResource { return permissionName.startsWith(GROUP_PREFIX); } - /** * check if the actual user is permitted to manage the repository permissions * return the repository if the user is permitted diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index b884a37771..e8c303e0f8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -39,7 +39,7 @@ public class RepositoryResource { private final Provider changesetRootResource; private final Provider sourceRootResource; private final Provider contentResource; - private final Provider permissionRootResource; + private final Provider permissionRootResource; private final Provider diffRootResource; private final Provider modificationsRootResource; private final Provider fileHistoryRootResource; @@ -54,7 +54,7 @@ public class RepositoryResource { Provider branchRootResource, Provider changesetRootResource, Provider sourceRootResource, Provider contentResource, - Provider permissionRootResource, + Provider permissionRootResource, Provider diffRootResource, Provider modificationsRootResource, Provider fileHistoryRootResource, @@ -154,7 +154,6 @@ public class RepositoryResource { private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) { Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId()); - changedRepository.setPermissions(existing.getPermissions()); return changedRepository; } @@ -194,7 +193,7 @@ public class RepositoryResource { } @Path("permissions/") - public PermissionRootResource permissions() { + public RepositoryPermissionRootResource permissions() { return permissionRootResource.get(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index be036007be..644b3adc3f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -514,7 +514,7 @@ class ResourceLinks { private final LinkBuilder permissionLinkBuilder; RepositoryPermissionLinks(ScmPathInfo pathInfo) { - permissionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class); + permissionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, RepositoryPermissionRootResource.class); } String all(String namespace, String name) { @@ -639,14 +639,30 @@ class ResourceLinks { } static class PermissionsLinks { - private final LinkBuilder permissionsLlinkBuilder; + private final LinkBuilder permissionsLinkBuilder; PermissionsLinks(ScmPathInfo scmPathInfo) { - this.permissionsLlinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class); + this.permissionsLinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class); } String self() { - return permissionsLlinkBuilder.method("getAll").parameters().href(); + return permissionsLinkBuilder.method("getAll").parameters().href(); + } + } + + public AvailableRepositoryPermissionLinks availableRepositoryPermissions() { + return new AvailableRepositoryPermissionLinks(scmPathInfoStore.get()); + } + + static class AvailableRepositoryPermissionLinks { + private final LinkBuilder linkBuilder; + + AvailableRepositoryPermissionLinks(ScmPathInfo scmPathInfo) { + this.linkBuilder = new LinkBuilder(scmPathInfo, RepositoryPermissionResource.class); + } + + String self() { + return linkBuilder.method("get").parameters().href(); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index a5d99c2928..3fdbcdf351 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -198,8 +198,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectRepositoryPermissions(Builder builder, Repository repository, User user, GroupNames groups) { - Collection repositoryPermissions - = repository.getPermissions(); + Collection repositoryPermissions = repository.getPermissions(); if (Util.isNotEmpty(repositoryPermissions)) { @@ -207,9 +206,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector for (RepositoryPermission permission : repositoryPermissions) { hasPermission = isUserPermitted(user, groups, permission); - if (hasPermission) + if (hasPermission && !permission.getVerbs().isEmpty()) { - String perm = permission.getType().getPermissionPrefix().concat(repository.getId()); + String perm = "repository:" + String.join(",", permission.getVerbs()) + ":" + repository.getId(); if (logger.isTraceEnabled()) { logger.trace("add repository permission {} for user {} at repository {}", diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java new file mode 100644 index 0000000000..0a508753bd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/RepositoryPermissionProvider.java @@ -0,0 +1,130 @@ +package sonia.scm.security; + +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.plugin.PluginLoader; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +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.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Collections.unmodifiableCollection; + +public class RepositoryPermissionProvider { + + private static final Logger logger = LoggerFactory.getLogger(RepositoryPermissionProvider.class); + private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml"; + private final Collection availableVerbs; + private final Collection availableRoles; + + @Inject + public RepositoryPermissionProvider(PluginLoader pluginLoader) { + AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader); + this.availableVerbs = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableVerbs)); + this.availableRoles = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs)).collect(Collectors.toList()))); + } + + public Collection availableVerbs() { + return availableVerbs; + } + + public Collection availableRoles() { + return availableRoles; + } + + private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) { + Collection availableVerbs = new ArrayList<>(); + Collection availableRoles = new ArrayList<>(); + + try { + JAXBContext context = + JAXBContext.newInstance(RepositoryPermissionsRoot.class); + + // Querying permissions from uberClassLoader returns also the permissions from plugin + Enumeration descriptorEnum = + pluginLoader.getUberClassLoader().getResources(REPOSITORY_PERMISSION_DESCRIPTOR); + + while (descriptorEnum.hasMoreElements()) { + URL descriptorUrl = descriptorEnum.nextElement(); + + logger.debug("read repository permission descriptor from {}", descriptorUrl); + + RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, descriptorUrl); + availableVerbs.addAll(repositoryPermissionsRoot.verbs.verbs); + availableRoles.addAll(repositoryPermissionsRoot.roles.roles); + } + } catch (IOException ex) { + logger.error("could not read permission descriptors", ex); + } catch (JAXBException ex) { + logger.error( + "could not create jaxb context to read permission descriptors", ex); + } + + return new AvailableRepositoryPermissions(availableVerbs, availableRoles); + } + + @SuppressWarnings("unchecked") + private static RepositoryPermissionsRoot parsePermissionDescriptor(JAXBContext context, URL descriptorUrl) { + try { + RepositoryPermissionsRoot descriptorWrapper = + (RepositoryPermissionsRoot) context.createUnmarshaller().unmarshal( + descriptorUrl); + logger.trace("repository permissions from {}: {}", descriptorUrl, descriptorWrapper.verbs.verbs); + logger.trace("repository roles from {}: {}", descriptorUrl, descriptorWrapper.roles.roles); + return descriptorWrapper; + } catch (JAXBException ex) { + logger.error("could not parse permission descriptor", ex); + return new RepositoryPermissionsRoot(); + } + } + + private static class AvailableRepositoryPermissions { + private final Collection availableVerbs; + private final Collection availableRoles; + + private AvailableRepositoryPermissions(Collection availableVerbs, Collection availableRoles) { + this.availableVerbs = unmodifiableCollection(availableVerbs); + this.availableRoles = unmodifiableCollection(availableRoles); + } + } + + @XmlRootElement(name = "repository-permissions") + @XmlAccessorType(XmlAccessType.FIELD) + private static class RepositoryPermissionsRoot { + private VerbListDescriptor verbs = new VerbListDescriptor(); + private RoleListDescriptor roles = new RoleListDescriptor(); + } + + @XmlRootElement(name = "verbs") + private static class VerbListDescriptor { + @XmlElement(name = "verb") + private List verbs = new ArrayList<>(); + } + + @XmlRootElement(name = "roles") + private static class RoleListDescriptor { + @XmlElement(name = "role") + private List roles = new ArrayList<>(); + } + + @XmlRootElement(name = "role") + @XmlAccessorType(XmlAccessType.FIELD) + public static class RoleDescriptor { + @XmlElement(name = "name") + private String name; + @XmlElement(name = "verbs") + private VerbListDescriptor verbs = new VerbListDescriptor(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java new file mode 100644 index 0000000000..6b6b06aa9c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/RepositoryRole.java @@ -0,0 +1,44 @@ +package sonia.scm.security; + +import org.apache.commons.collections.CollectionUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +public class RepositoryRole { + + private final String name; + private final Collection verbs; + + public RepositoryRole(String name, Collection verbs) { + this.name = name; + this.verbs = verbs; + } + + public String getName() { + return name; + } + + public Collection getVerbs() { + return Collections.unmodifiableCollection(verbs); + } + + public String toString() { + return "Role " + name + " (" + String.join(", ", verbs) + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RepositoryRole)) return false; + RepositoryRole that = (RepositoryRole) o; + return name.equals(that.name) && + CollectionUtils.isEqualCollection(this.verbs, that.verbs); + } + + @Override + public int hashCode() { + return Objects.hash(name, verbs.size()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java index 3eaa684080..b442042480 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java +++ b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutor.java @@ -35,6 +35,7 @@ package sonia.scm.web.cgi; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; @@ -139,12 +140,6 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor apendOsEnvironment(env); } - // workaround for mercurial 2.1 - if (isContentLengthWorkaround()) - { - env.set(ENV_CONTENT_LENGTH, Integer.toString(request.getContentLength())); - } - if (workDirectory == null) { workDirectory = command.getParentFile(); @@ -304,26 +299,10 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor String uri = HttpUtil.removeMatrixParameter(request.getRequestURI()); String scriptName = uri.substring(0, uri.length() - pathInfo.length()); String scriptPath = context.getRealPath(scriptName); - int len = request.getContentLength(); EnvList env = new EnvList(); env.set(ENV_AUTH_TYPE, request.getAuthType()); - - /** - * Note CGI spec says CONTENT_LENGTH must be NULL ("") or undefined - * if there is no content, so we cannot put 0 or -1 in as per the - * Servlet API spec. - * - * see org.apache.catalina.servlets.CGIServlet - */ - if (len <= 0) - { - env.set(ENV_CONTENT_LENGTH, ""); - } - else - { - env.set(ENV_CONTENT_LENGTH, Integer.toString(len)); - } + env.set(ENV_CONTENT_LENGTH, createCGIContentLength(request, contentLengthWorkaround)); /** * Decode PATH_INFO @@ -383,6 +362,39 @@ public class DefaultCGIExecutor extends AbstractCGIExecutor return env; } + /** + * Returns the content length as string in the cgi specific format. + * + * CGI spec says CONTENT_LENGTH must be NULL ("") or undefined + * if there is no content, so we cannot put 0 or -1 in as per the + * Servlet API spec. Some CGI applications require a content + * length environment variable, which is not null or empty + * (e.g. mercurial). For those application the disallowEmptyResults + * parameter should be used. + * + * @param disallowEmptyResults {@code true} to return -1 instead of an empty string + * + * @return content length as cgi specific string + */ + @VisibleForTesting + static String createCGIContentLength(HttpServletRequest request, boolean disallowEmptyResults) { + String cgiContentLength = disallowEmptyResults ? "-1" : ""; + + String contentLength = request.getHeader("Content-Length"); + if (!Strings.isNullOrEmpty(contentLength)) { + try { + long len = Long.parseLong(contentLength); + if (len > 0) { + cgiContentLength = String.valueOf(len); + } + } catch (NumberFormatException ex) { + logger.warn("received request with invalid content-length header value: {}", contentLength); + } + } + + return cgiContentLength; + } + /** * Method description * diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index c6a96e4b9e..250846a8cc 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -4,6 +4,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; +import org.apache.shiro.authz.AuthorizationException; import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; import sonia.scm.filter.WebElement; @@ -74,6 +75,9 @@ public class HttpProtocolServlet extends HttpServlet { } catch (NotFoundException e) { log.debug(e.getMessage()); resp.setStatus(HttpStatus.SC_NOT_FOUND); + } catch (AuthorizationException e) { + log.debug(e.getMessage()); + resp.setStatus(HttpStatus.SC_FORBIDDEN); } } } diff --git a/scm-webapp/src/main/resources/META-INF/scm/repository-permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/repository-permissions.xml new file mode 100644 index 0000000000..4646482fe7 --- /dev/null +++ b/scm-webapp/src/main/resources/META-INF/scm/repository-permissions.xml @@ -0,0 +1,42 @@ + + + read + modify + delete + pull + push + permissionRead + permissionWrite + healthCheck + * + + + + READ + + read + pull + + + + WRITE + + read + pull + push + + + + HEALTH + + healthCheck + + + + OWNER + + * + + + + diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 7c75539005..67f26115dc 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -37,5 +37,45 @@ } }, "unknown": "Unknown permission" + }, + "verbs": { + "repository": { + "read": { + "displayName": "read", + "description": "May see the repository inside the SCM-Manager" + }, + "modify": { + "displayName": "modify", + "description": "May modify the properties of the repository" + }, + "delete": { + "displayName": "delete", + "description": "May delete the repository" + }, + "pull": { + "displayName": "pull/checkout", + "description": "May pull/checkout the repository" + }, + "push": { + "displayName": "push/commit", + "description": "May change the content of the repository (push/commit)" + }, + "permissionRead": { + "displayName": "read permissions", + "description": "May see the permissions of the repository" + }, + "permissionWrite": { + "displayName": "modify permissions", + "description": "May modify the permissions of the repository" + }, + "healthCheck": { + "displayName": "health check", + "description": "May run the health check for the repository" + }, + "*": { + "displayName": "overall", + "description": "May change everything for the repository (includes all other permissions)" + } + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index 41bcac3c6a..696174d6e0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -12,10 +12,14 @@ import sonia.scm.ModelObject; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Request; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Collection; import java.util.Comparator; import static java.util.Collections.emptyList; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; @@ -25,8 +29,13 @@ public class AbstractManagerResourceTest { @Mock private Manager manager; + @Mock private Request request; + + @Mock + private UriInfo uriInfo; + @Captor private ArgumentCaptor> comparatorCaptor; @@ -59,6 +68,24 @@ public class AbstractManagerResourceTest { abstractManagerResource.getAll(request, 0, 1, "x", true); } + @Test + public void testLocation() throws URISyntaxException { + URI uri = location("special-item"); + assertEquals(new URI("https://scm.scm-manager.org/simple/special-item"), uri); + } + + @Test + public void testLocationWithSpaces() throws URISyntaxException { + URI uri = location("Scm Special Group"); + assertEquals(new URI("https://scm.scm-manager.org/simple/Scm%20Special%20Group"), uri); + } + + private URI location(String id) throws URISyntaxException { + URI base = new URI("https://scm.scm-manager.org/"); + when(uriInfo.getAbsolutePath()).thenReturn(base); + + return abstractManagerResource.location(uriInfo, id); + } private class SimpleManagerResource extends AbstractManagerResource { @@ -82,7 +109,7 @@ public class AbstractManagerResourceTest { @Override protected String getPathPart() { - return null; + return "simple"; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java similarity index 89% rename from scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java rename to scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index 012656c4cd..2795562b14 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; @@ -29,10 +30,9 @@ import org.junit.jupiter.api.TestFactory; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.web.VndMediaType; import java.io.IOException; @@ -47,6 +47,8 @@ import java.util.stream.Stream; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -66,7 +68,7 @@ import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class PermissionRootResourceTest extends RepositoryTestBase { +public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAME = "repo"; private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME; @@ -76,15 +78,15 @@ public class PermissionRootResourceTest extends RepositoryTestBase { private static final String PERMISSION_NAME = "perm"; private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/"; private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME; - private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }"; + private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"verbs\" : [\"read\",\"pull\"] }"; private static final ArrayList TEST_PERMISSIONS = Lists .newArrayList( - new RepositoryPermission("user_write", PermissionType.WRITE, false), - new RepositoryPermission("user_read", PermissionType.READ, false), - new RepositoryPermission("user_owner", PermissionType.OWNER, false), - new RepositoryPermission("group_read", PermissionType.READ, true), - new RepositoryPermission("group_write", PermissionType.WRITE, true), - new RepositoryPermission("group_owner", PermissionType.OWNER, true) + new RepositoryPermission("user_write", asList("read","modify"), false), + new RepositoryPermission("user_read", singletonList("read"), false), + new RepositoryPermission("user_owner", singletonList("*"), false), + new RepositoryPermission("group_read", singletonList("read"), true), + new RepositoryPermission("group_write", asList("read","modify"), true), + new RepositoryPermission("group_owner", singletonList("*"), true) ); private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() .description("GET all permissions") @@ -124,11 +126,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase { private RepositoryPermissionToRepositoryPermissionDtoMapperImpl permissionToPermissionDtoMapper; @InjectMocks - private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; + private RepositoryPermissionDtoToRepositoryPermissionMapperImpl permissionDtoToPermissionMapper; private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; - private PermissionRootResource permissionRootResource; + private RepositoryPermissionRootResource repositoryPermissionRootResource; private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -138,8 +140,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() { initMocks(this); repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); - permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager); - super.permissionRootResource = Providers.of(permissionRootResource); + repositoryPermissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager); + super.permissionRootResource = Providers.of(repositoryPermissionRootResource); dispatcher = createDispatcher(getRepositoryRootResource()); subjectThreadState.bind(); ThreadContext.bind(subject); @@ -232,11 +234,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase { public void shouldGet400OnCreatingNewPermissionWithNotAllowedCharacters() throws URISyntaxException { // the @ character at the begin of the name is not allowed createUserWithRepository("user"); - String permissionJson = "{ \"name\": \"@permission\", \"type\": \"OWNER\" }"; + String permissionJson = "{ \"name\": \"@permission\", \"verbs\": [\"*\"] }"; MockHttpRequest request = MockHttpRequest .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS) .content(permissionJson.getBytes()) - .contentType(VndMediaType.PERMISSION); + .contentType(VndMediaType.REPOSITORY_PERMISSION); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -244,11 +246,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase { assertEquals(400, response.getStatus()); // the whitespace at the begin opf the name is not allowed - permissionJson = "{ \"name\": \" permission\", \"type\": \"OWNER\" }"; + permissionJson = "{ \"name\": \" permission\", \"verbs\": [\"*\"] }"; request = MockHttpRequest .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS) .content(permissionJson.getBytes()) - .contentType(VndMediaType.PERMISSION); + .contentType(VndMediaType.REPOSITORY_PERMISSION); response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -259,12 +261,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void shouldGetCreatedPermissions() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); - RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", PermissionType.WRITE, true); + RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", asList("read", "pull", "push"), true); ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); permissions.add(newPermission); ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); assertExpectedRequest(requestPOSTPermission - .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : true}") .expectedResponseStatus(201) .responseValidator(response -> assertThat(response.getContentAsString()) .as("POST response has no body") @@ -278,7 +280,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); RepositoryPermission newPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestPOSTPermission - .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : false}") + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : false}") .expectedResponseStatus(409) ); } @@ -288,10 +290,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); RepositoryPermission modifiedPermission = TEST_PERMISSIONS.get(0); // modify the type to owner - modifiedPermission.setType(PermissionType.OWNER); + modifiedPermission.setVerbs(new ArrayList<>(singletonList("*"))); ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); assertExpectedRequest(requestPUTPermission - .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") + .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"verbs\" : [\"*\"], \"groupPermission\" : false}") .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) .expectedResponseStatus(204) .responseValidator(response -> assertThat(response.getContentAsString()) @@ -353,7 +355,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase { .map(hal -> { RepositoryPermissionDto result = new RepositoryPermissionDto(); result.setName(hal.getAttribute("name").asText()); - result.setType(hal.getAttribute("type").asText()); + JsonNode attribute = hal.getAttribute("verbs"); + List verbs = new ArrayList<>(); + attribute.iterator().forEachRemaining(v -> verbs.add(v.asText())); + result.setVerbs(verbs); result.setGroupPermission(hal.getAttribute("groupPermission").asBoolean()); result.add(hal.getLinks()); return result; @@ -382,7 +387,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { RepositoryPermissionDto result = new RepositoryPermissionDto(); result.setName(permission.getName()); result.setGroupPermission(permission.isGroupPermission()); - result.setType(permission.getType().name()); + result.setVerbs(permission.getVerbs()); String permissionName = Optional.of(permission.getName()) .filter(p -> !permission.isGroupPermission()) .orElse(GROUP_PREFIX + permission.getName()); @@ -425,7 +430,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { HttpRequest request = MockHttpRequest .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) .content(entry.content) - .contentType(VndMediaType.PERMISSION); + .contentType(VndMediaType.REPOSITORY_PERMISSION); dispatcher.invoke(request, response); log.info("Test the Request :{}", entry); assertThat(response.getStatus()) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java index a6ab00db58..97146e67ff 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java @@ -8,11 +8,11 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import java.net.URI; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; @RunWith(MockitoJUnitRunner.Silent.class) @@ -36,7 +36,7 @@ public class RepositoryPermissionToRepositoryPermissionDtoMapperTest { @SubjectAware(username = "trillian", password = "secret") public void shouldMapGroupPermissionCorrectly() { Repository repository = getDummyRepository(); - RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, true); + RepositoryPermission permission = new RepositoryPermission("42", asList("read","modify","delete"), true); RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository); @@ -48,7 +48,7 @@ public class RepositoryPermissionToRepositoryPermissionDtoMapperTest { @SubjectAware(username = "trillian", password = "secret") public void shouldMapNonGroupPermissionCorrectly() { Repository repository = getDummyRepository(); - RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, false); + RepositoryPermission permission = new RepositoryPermission("42", asList("read","modify","delete"), false); RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 1677be95b1..bf4366f0b2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -6,7 +6,6 @@ import com.google.common.io.Resources; import com.google.inject.util.Providers; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; -import org.assertj.core.api.Assertions; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; @@ -18,8 +17,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryManager; @@ -41,15 +38,12 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -291,36 +285,14 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { dispatcher.invoke(request, response); - Assertions.assertThat(createCaptor.getValue().getPermissions()) + assertThat(createCaptor.getValue().getPermissions()) .hasSize(1) .allSatisfy(p -> { assertThat(p.getName()).isEqualTo("trillian"); - assertThat(p.getType()).isEqualTo(PermissionType.OWNER); + assertThat(p.getVerbs()).containsExactly("*"); }); } - @Test - public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception { - Repository existingRepository = mockRepository("space", "repo"); - existingRepository.setPermissions(singletonList(new RepositoryPermission("user", PermissionType.READ))); - - URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); - byte[] repository = Resources.toByteArray(url); - - ArgumentCaptor modifiedRepositoryCaptor = forClass(Repository.class); - doNothing().when(repositoryManager).modify(modifiedRepositoryCaptor.capture()); - - MockHttpRequest request = MockHttpRequest - .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") - .contentType(VndMediaType.REPOSITORY) - .content(repository); - MockHttpResponse response = new MockHttpResponse(); - - dispatcher.invoke(request, response); - - assertFalse(modifiedRepositoryCaptor.getValue().getPermissions().isEmpty()); - } - @Test public void shouldCreateArrayOfProtocolUrls() throws Exception { mockRepository("space", "repo"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index b2daf0536c..a8901e2d79 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -16,7 +16,7 @@ public abstract class RepositoryTestBase { protected Provider changesetRootResource; protected Provider sourceRootResource; protected Provider contentResource; - protected Provider permissionRootResource; + protected Provider permissionRootResource; protected Provider diffRootResource; protected Provider modificationsRootResource; protected Provider fileHistoryRootResource; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 9bf70093fd..8469e966c8 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -10,8 +10,6 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; -import sonia.scm.repository.RepositoryPermission; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; @@ -238,7 +236,6 @@ public class RepositoryToRepositoryDtoMapperTest { repository.setId("1"); repository.setCreationDate(System.currentTimeMillis()); repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure"))); - repository.setPermissions(singletonList(new RepositoryPermission("permission", PermissionType.READ))); return repository; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 655d00fc10..8714496e1a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -42,6 +42,7 @@ public class ResourceLinksMock { when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); + when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo)); return resourceLinks; } diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java index c55a33c39a..8cdb162740 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java @@ -50,7 +50,6 @@ import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.api.v2.resources.RepositoryDto; import sonia.scm.api.v2.resources.UserDto; import sonia.scm.api.v2.resources.UserToUserDtoMapperImpl; -import sonia.scm.repository.PermissionType; import sonia.scm.user.User; import sonia.scm.user.UserTestData; import sonia.scm.util.HttpUtil; @@ -117,10 +116,6 @@ public class GitLfsITCase { @Test public void testLfsAPIWithOwnerPermissions() throws IOException { - uploadAndDownloadAsUser(PermissionType.OWNER); - } - - private void uploadAndDownloadAsUser(PermissionType permissionType) throws IOException { User trillian = UserTestData.createTrillian(); trillian.setPassword("secret123"); createUser(trillian); @@ -129,8 +124,8 @@ public class GitLfsITCase { String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) .accept("*/*") - .type(VndMediaType.PERMISSION) - .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"WRITE\"}"); + .type(VndMediaType.REPOSITORY_PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"*\"]}"); ScmClient client = new ScmClient(trillian.getId(), "secret123"); @@ -140,11 +135,6 @@ public class GitLfsITCase { } } - @Test - public void testLfsAPIWithWritePermissions() throws IOException { - uploadAndDownloadAsUser(PermissionType.WRITE); - } - private void createUser(User user) { UserDto dto = new UserToUserDtoMapperImpl(){ @Override @@ -175,8 +165,8 @@ public class GitLfsITCase { String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) .accept("*/*") - .type(VndMediaType.PERMISSION) - .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); + .type(VndMediaType.REPOSITORY_PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"read\"]}"); ScmClient client = new ScmClient(trillian.getId(), "secret123"); uploadAndDownload(client); @@ -196,8 +186,8 @@ public class GitLfsITCase { String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) .accept("*/*") - .type(VndMediaType.PERMISSION) - .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); + .type(VndMediaType.REPOSITORY_PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"read\",\"pull\"]}"); // upload data as admin String data = UUID.randomUUID().toString(); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 146810e787..3d00fcaa7e 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -111,7 +111,6 @@ public class DefaultRepositoryManagerPerfTest { public void setUpObjectUnderTest(){ when(repositoryHandler.getType()).thenReturn(new RepositoryType(REPOSITORY_TYPE, REPOSITORY_TYPE, Sets.newHashSet())); Set handlerSet = ImmutableSet.of(repositoryHandler); - RepositoryMatcher repositoryMatcher = new RepositoryMatcher(Collections.emptySet()); NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); repositoryManager = new DefaultRepositoryManager( configuration, @@ -138,7 +137,7 @@ public class DefaultRepositoryManagerPerfTest { /** * Start performance test and ensure that the timeout is not reached. */ - @Test(timeout = 6000l) + @Test(timeout = 6000L) public void perfTestGetAll(){ SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret")); @@ -155,7 +154,7 @@ public class DefaultRepositoryManagerPerfTest { } private long calculateAverage(List times) { - Long sum = 0l; + Long sum = 0L; if(!times.isEmpty()) { for (Long time : times) { sum += time; @@ -183,9 +182,8 @@ private long calculateAverage(List times) { } private Repository createTestRepository(int number) { - Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number); - repository.addPermission(new RepositoryPermission("trillian", PermissionType.READ)); - return repository; + return new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number); + } static class DummyRealm extends AuthorizingRealm { diff --git a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java index ab8ce5dce8..59b6951025 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java @@ -32,14 +32,12 @@ package sonia.scm.security; import com.google.common.collect.Lists; -import org.junit.Test; -import static org.junit.Assert.*; import org.junit.Before; +import org.junit.Test; import sonia.scm.HandlerEventType; import sonia.scm.group.Group; import sonia.scm.group.GroupEvent; import sonia.scm.group.GroupModificationEvent; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryModificationEvent; @@ -50,6 +48,14 @@ import sonia.scm.user.UserEvent; import sonia.scm.user.UserModificationEvent; import sonia.scm.user.UserTestData; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + /** * Unit tests for {@link AuthorizationChangedEventProducer}. * @@ -87,7 +93,12 @@ public class AuthorizationChangedEventProducerTest { assertTrue(producer.event.isEveryUserAffected()); assertEquals(username, producer.event.getNameOfAffectedUser()); } - + + private void assertGlobalEventIsFired(){ + assertNotNull(producer.event); + assertFalse(producer.event.isEveryUserAffected()); + } + /** * Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)} with modified user. */ @@ -127,11 +138,6 @@ public class AuthorizationChangedEventProducerTest { assertGlobalEventIsFired(); } - private void assertGlobalEventIsFired(){ - assertNotNull(producer.event); - assertFalse(producer.event.isEveryUserAffected()); - } - /** * Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups. */ @@ -174,40 +180,49 @@ public class AuthorizationChangedEventProducerTest { { Repository repositoryModified = RepositoryTestData.createHeartOfGold(); repositoryModified.setName("test123"); - repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test"))); - + repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false))); + Repository repository = RepositoryTestData.createHeartOfGold(); - repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test"))); - + repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false))); + producer.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository)); assertEventIsNotFired(); - + producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertEventIsNotFired(); - - repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test"))); + + repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false))); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertEventIsNotFired(); - - repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123"))); + + repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123", singletonList("read"), false))); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertGlobalEventIsFired(); - + resetStoredEvent(); repositoryModified.setPermissions( - Lists.newArrayList(new RepositoryPermission("test", PermissionType.READ, true)) + Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), true)) ); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertGlobalEventIsFired(); - + resetStoredEvent(); - + repositoryModified.setPermissions( - Lists.newArrayList(new RepositoryPermission("test", PermissionType.WRITE)) + Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false)) ); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertGlobalEventIsFired(); + + resetStoredEvent(); + repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false))); + + repositoryModified.setPermissions( + Lists.newArrayList(new RepositoryPermission("test", asList("write", "read"), false)) + ); + producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); + assertEventIsNotFired(); } private void resetStoredEvent(){ diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 3b3a28861f..e9345c9599 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -51,7 +51,6 @@ import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupNames; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryPermission; @@ -59,6 +58,7 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import static java.util.Arrays.asList; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; @@ -225,10 +225,10 @@ public class DefaultAuthorizationCollectorTest { authenticate(UserTestData.createTrillian(), group); Repository heartOfGold = RepositoryTestData.createHeartOfGold(); heartOfGold.setId("one"); - heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian"))); + heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian", asList("read", "pull"), false))); Repository puzzle42 = RepositoryTestData.create42Puzzle(); puzzle42.setId("two"); - RepositoryPermission permission = new RepositoryPermission(group, PermissionType.WRITE, true); + RepositoryPermission permission = new RepositoryPermission(group, asList("read", "pull", "push"), true); puzzle42.setPermissions(Lists.newArrayList(permission)); when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42)); diff --git a/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java b/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java new file mode 100644 index 0000000000..b5998084f8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/RepositoryPermissionProviderTest.java @@ -0,0 +1,58 @@ +package sonia.scm.security; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.util.ClassLoaders; + +import java.lang.reflect.Field; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RepositoryPermissionProviderTest { + + private RepositoryPermissionProvider repositoryPermissionProvider; + private String[] allVerbsFromRepositoryClass; + + + @BeforeEach + void init() { + PluginLoader pluginLoader = mock(PluginLoader.class); + when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); + repositoryPermissionProvider = new RepositoryPermissionProvider(pluginLoader); + allVerbsFromRepositoryClass = Arrays.stream(RepositoryPermissions.class.getDeclaredFields()) + .filter(field -> field.getName().startsWith("ACTION_")) + .map(this::getString) + .filter(verb -> !"create".equals(verb)) + .toArray(String[]::new); + } + + @Test + void shouldReadAvailableRoles() { + assertThat(repositoryPermissionProvider.availableRoles()).isNotEmpty(); + assertThat(repositoryPermissionProvider.availableRoles()).allSatisfy(this::containsOnlyAvailableVerbs); + } + + private void containsOnlyAvailableVerbs(RepositoryRole role) { + assertThat(role.getVerbs()).isSubsetOf(repositoryPermissionProvider.availableVerbs()); + } + + @Test + void shouldReadAvailableVerbsFromRepository() { + assertThat(repositoryPermissionProvider.availableVerbs()).contains(allVerbsFromRepositoryClass); + } + + private String getString(Field field) { + try { + return (String) field.get(null); + } catch (IllegalAccessException e) { + fail(e); + return null; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java new file mode 100644 index 0000000000..29c7dea358 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/cgi/DefaultCGIExecutorTest.java @@ -0,0 +1,54 @@ +package sonia.scm.web.cgi; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DefaultCGIExecutor}. + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultCGIExecutorTest { + + @Mock + private HttpServletRequest request; + + @Test + public void testCreateCGIContentLength() { + when(request.getHeader("Content-Length")).thenReturn("42"); + assertEquals("42", DefaultCGIExecutor.createCGIContentLength(request, false)); + assertEquals("42", DefaultCGIExecutor.createCGIContentLength(request, true)); + } + + @Test + public void testCreateCGIContentLengthWithZeroLength() { + when(request.getHeader("Content-Length")).thenReturn("0"); + assertEquals("", DefaultCGIExecutor.createCGIContentLength(request, false)); + assertEquals("-1", DefaultCGIExecutor.createCGIContentLength(request, true)); + } + + @Test + public void testCreateCGIContentLengthWithoutContentLengthHeader() { + assertEquals("", DefaultCGIExecutor.createCGIContentLength(request, false)); + assertEquals("-1", DefaultCGIExecutor.createCGIContentLength(request, true)); + } + + @Test + public void testCreateCGIContentLengthWithLengthThatExeedsInteger() { + when(request.getHeader("Content-Length")).thenReturn("6314297259"); + assertEquals("6314297259", DefaultCGIExecutor.createCGIContentLength(request, false)); + } + + @Test + public void testCreateCGIContentLengthWithNonNumberHeader() { + when(request.getHeader("Content-Length")).thenReturn("abc"); + assertEquals("", DefaultCGIExecutor.createCGIContentLength(request, false)); + } + +}