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/api/v2/resources/BaseMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java index 89cc893131..eba8173de1 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java @@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import org.mapstruct.Mapping; -public abstract class BaseMapper extends LinkAppenderMapper implements InstantAttributeMapper { +public abstract class BaseMapper extends HalAppenderMapper implements InstantAttributeMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract D map(T modelObject); diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java index 162d5d6699..6759f5cb8c 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -7,7 +8,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; import java.time.Instant; -import java.util.List; @Getter @Setter @@ -34,16 +34,7 @@ public class ChangesetDto extends HalRepresentation { */ private String description; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + public ChangesetDto(Links links, Embedded embedded) { + super(links, embedded); } - - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation withEmbedded(String rel, List halRepresentations) { - return super.withEmbedded(rel, halRepresentations); - } - - } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppender.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java similarity index 61% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppender.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java index d3864dc798..a7beaf1f6e 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppender.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppender.java @@ -1,12 +1,14 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.HalRepresentation; + /** - * The {@link LinkAppender} can be used within an {@link LinkEnricher} to append hateoas links to a json response. + * The {@link HalAppender} can be used within an {@link HalEnricher} to append hateoas links to a json response. * * @author Sebastian Sdorra * @since 2.0.0 */ -public interface LinkAppender { +public interface HalAppender { /** * Appends one link to the json response. @@ -14,7 +16,7 @@ public interface LinkAppender { * @param rel name of relation * @param href link uri */ - void appendOne(String rel, String href); + void appendLink(String rel, String href); /** * Returns a builder which is able to append an array of links to the resource. @@ -22,8 +24,15 @@ public interface LinkAppender { * @param rel name of link relation * @return multi link builder */ - LinkArrayBuilder arrayBuilder(String rel); + LinkArrayBuilder linkArrayBuilder(String rel); + /** + * Appends one embedded to the json response. + * + * @param rel name of relation + * @param embeddedItem embedded object + */ + void appendEmbedded(String rel, HalRepresentation embeddedItem); /** * Builder for link arrays. diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppenderMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java similarity index 56% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppenderMapper.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java index 7843491b71..dd49b765bc 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkAppenderMapper.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalAppenderMapper.java @@ -4,17 +4,17 @@ import com.google.common.annotations.VisibleForTesting; import javax.inject.Inject; -public class LinkAppenderMapper { +public class HalAppenderMapper { @Inject - private LinkEnricherRegistry registry; + private HalEnricherRegistry registry; @VisibleForTesting - void setRegistry(LinkEnricherRegistry registry) { + void setRegistry(HalEnricherRegistry registry) { this.registry = registry; } - protected void appendLinks(LinkAppender appender, Object source, Object... contextEntries) { + protected void applyEnrichers(HalAppender appender, Object source, Object... contextEntries) { // null check is only their to not break existing tests if (registry != null) { @@ -24,10 +24,10 @@ public class LinkAppenderMapper { ctx[i + 1] = contextEntries[i]; } - LinkEnricherContext context = LinkEnricherContext.of(ctx); + HalEnricherContext context = HalEnricherContext.of(ctx); - Iterable enrichers = registry.allByType(source.getClass()); - for (LinkEnricher enricher : enrichers) { + Iterable enrichers = registry.allByType(source.getClass()); + for (HalEnricher enricher : enrichers) { enricher.enrich(context, appender); } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricher.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java similarity index 51% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricher.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java index c16d6f6482..647a1cf74e 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricher.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricher.java @@ -3,8 +3,8 @@ package sonia.scm.api.v2.resources; import sonia.scm.plugin.ExtensionPoint; /** - * A {@link LinkEnricher} can be used to append hateoas links to a specific json response. - * To register an enricher use the {@link Enrich} annotation or the {@link LinkEnricherRegistry} which is available + * A {@link HalEnricher} can be used to append hal specific attributes, such as links, to the json response. + * To register an enricher use the {@link Enrich} annotation or the {@link HalEnricherRegistry} which is available * via injection. * * Warning: enrichers are always registered as singletons. @@ -14,13 +14,13 @@ import sonia.scm.plugin.ExtensionPoint; */ @ExtensionPoint @FunctionalInterface -public interface LinkEnricher { +public interface HalEnricher { /** - * Enriches the response with hateoas links. + * Enriches the response with hal specific attributes. * * @param context contains the source for the json mapping and related objects - * @param appender can be used to append links to the json response + * @param appender can be used to append links or embedded objects to the json response */ - void enrich(LinkEnricherContext context, LinkAppender appender); + void enrich(HalEnricherContext context, HalAppender appender); } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherContext.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java similarity index 80% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherContext.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java index 2808a923e9..36128087b8 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherContext.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherContext.java @@ -7,17 +7,17 @@ import java.util.NoSuchElementException; import java.util.Optional; /** - * Context object for the {@link LinkEnricher}. The context holds the source object for the json and all related - * objects, which can be useful for the link creation. + * Context object for the {@link HalEnricher}. The context holds the source object for the json and all related + * objects, which can be useful for the enrichment. * * @author Sebastian Sdorra * @since 2.0.0 */ -public final class LinkEnricherContext { +public final class HalEnricherContext { private final Map instanceMap; - private LinkEnricherContext(Map instanceMap) { + private HalEnricherContext(Map instanceMap) { this.instanceMap = instanceMap; } @@ -28,12 +28,12 @@ public final class LinkEnricherContext { * * @return context of given entries */ - public static LinkEnricherContext of(Object... instances) { + public static HalEnricherContext of(Object... instances) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Object instance : instances) { builder.put(instance.getClass(), instance); } - return new LinkEnricherContext(builder.build()); + return new HalEnricherContext(builder.build()); } /** diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherRegistry.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java similarity index 53% rename from scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherRegistry.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java index cd95a62ec3..3fadbfa388 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkEnricherRegistry.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/HalEnricherRegistry.java @@ -7,34 +7,34 @@ import sonia.scm.plugin.Extension; import javax.inject.Singleton; /** - * The {@link LinkEnricherRegistry} is responsible for binding {@link LinkEnricher} instances to their source types. + * The {@link HalEnricherRegistry} is responsible for binding {@link HalEnricher} instances to their source types. * * @author Sebastian Sdorra * @since 2.0.0 */ @Extension @Singleton -public final class LinkEnricherRegistry { +public final class HalEnricherRegistry { - private final Multimap enrichers = HashMultimap.create(); + private final Multimap enrichers = HashMultimap.create(); /** - * Registers a new {@link LinkEnricher} for the given source type. + * Registers a new {@link HalEnricher} for the given source type. * * @param sourceType type of json mapping source * @param enricher link enricher instance */ - public void register(Class sourceType, LinkEnricher enricher) { + public void register(Class sourceType, HalEnricher enricher) { enrichers.put(sourceType, enricher); } /** - * Returns all registered {@link LinkEnricher} for the given type. + * Returns all registered {@link HalEnricher} for the given type. * * @param sourceType type of json mapping source * @return all registered enrichers */ - public Iterable allByType(Class sourceType) { + public Iterable allByType(Class sourceType) { return enrichers.get(sourceType); } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java index bf20f26a7a..346ce83816 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/Index.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2.resources; /** - * The {@link Index} object can be used to register a {@link LinkEnricher} for the index resource. + * The {@link Index} object can be used to register a {@link HalEnricher} for the index resource. * * @author Sebastian Sdorra * @since 2.0.0 diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java index f8f82804a6..a027a78d79 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/Me.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2.resources; /** - * The {@link Me} object can be used to register a {@link LinkEnricher} for the me resource. + * The {@link Me} object can be used to register a {@link HalEnricher} for the me resource. * * @author Sebastian Sdorra * @since 2.0.0 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/api/v2/resources/LinkAppenderMapperTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java similarity index 53% rename from scm-core/src/test/java/sonia/scm/api/v2/resources/LinkAppenderMapperTest.java rename to scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java index 557eac2020..ff658cc26a 100644 --- a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkAppenderMapperTest.java +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java @@ -11,51 +11,51 @@ import java.util.Optional; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -class LinkAppenderMapperTest { +class HalAppenderMapperTest { @Mock - private LinkAppender appender; + private HalAppender appender; - private LinkEnricherRegistry registry; - private LinkAppenderMapper mapper; + private HalEnricherRegistry registry; + private HalAppenderMapper mapper; @BeforeEach void beforeEach() { - registry = new LinkEnricherRegistry(); - mapper = new LinkAppenderMapper(); + registry = new HalEnricherRegistry(); + mapper = new HalAppenderMapper(); mapper.setRegistry(registry); } @Test void shouldAppendSimpleLink() { - registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com")); - mapper.appendLinks(appender, "hello"); + mapper.applyEnrichers(appender, "hello"); - verify(appender).appendOne("42", "https://hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); } @Test void shouldCallMultipleEnrichers() { - registry.register(String.class, (ctx, appender) -> appender.appendOne("42", "https://hitchhiker.com")); - registry.register(String.class, (ctx, appender) -> appender.appendOne("21", "https://scm.hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com")); + registry.register(String.class, (ctx, appender) -> appender.appendLink("21", "https://scm.hitchhiker.com")); - mapper.appendLinks(appender, "hello"); + mapper.applyEnrichers(appender, "hello"); - verify(appender).appendOne("42", "https://hitchhiker.com"); - verify(appender).appendOne("21", "https://scm.hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); + verify(appender).appendLink("21", "https://scm.hitchhiker.com"); } @Test void shouldAppendLinkByUsingSourceFromContext() { registry.register(String.class, (ctx, appender) -> { Optional rel = ctx.oneByType(String.class); - appender.appendOne(rel.get(), "https://hitchhiker.com"); + appender.appendLink(rel.get(), "https://hitchhiker.com"); }); - mapper.appendLinks(appender, "42"); + mapper.applyEnrichers(appender, "42"); - verify(appender).appendOne("42", "https://hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); } @Test @@ -63,12 +63,12 @@ class LinkAppenderMapperTest { registry.register(Integer.class, (ctx, appender) -> { Optional rel = ctx.oneByType(Integer.class); Optional href = ctx.oneByType(String.class); - appender.appendOne(String.valueOf(rel.get()), href.get()); + appender.appendLink(String.valueOf(rel.get()), href.get()); }); - mapper.appendLinks(appender, Integer.valueOf(42), "https://hitchhiker.com"); + mapper.applyEnrichers(appender, Integer.valueOf(42), "https://hitchhiker.com"); - verify(appender).appendOne("42", "https://hitchhiker.com"); + verify(appender).appendLink("42", "https://hitchhiker.com"); } } diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherContextTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java similarity index 72% rename from scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherContextTest.java rename to scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java index 6eb7bb4c84..1aecb5ad46 100644 --- a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherContextTest.java +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java @@ -7,17 +7,17 @@ import org.junit.jupiter.api.Test; import java.util.NoSuchElementException; -class LinkEnricherContextTest { +class HalEnricherContextTest { @Test void shouldCreateContextFromSingleObject() { - LinkEnricherContext context = LinkEnricherContext.of("hello"); + HalEnricherContext context = HalEnricherContext.of("hello"); assertThat(context.oneByType(String.class)).contains("hello"); } @Test void shouldCreateContextFromMultipleObjects() { - LinkEnricherContext context = LinkEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L)); + HalEnricherContext context = HalEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L)); assertThat(context.oneByType(String.class)).contains("hello"); assertThat(context.oneByType(Integer.class)).contains(42); assertThat(context.oneByType(Long.class)).contains(21L); @@ -25,19 +25,19 @@ class LinkEnricherContextTest { @Test void shouldReturnEmptyOptionalForUnknownTypes() { - LinkEnricherContext context = LinkEnricherContext.of(); + HalEnricherContext context = HalEnricherContext.of(); assertThat(context.oneByType(String.class)).isNotPresent(); } @Test void shouldReturnRequiredObject() { - LinkEnricherContext context = LinkEnricherContext.of("hello"); + HalEnricherContext context = HalEnricherContext.of("hello"); assertThat(context.oneRequireByType(String.class)).isEqualTo("hello"); } @Test void shouldThrowAnNoSuchElementExceptionForUnknownTypes() { - LinkEnricherContext context = LinkEnricherContext.of(); + HalEnricherContext context = HalEnricherContext.of(); assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class)); } diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherRegistryTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java similarity index 53% rename from scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherRegistryTest.java rename to scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java index 07441003d7..6a863d2f04 100644 --- a/scm-core/src/test/java/sonia/scm/api/v2/resources/LinkEnricherRegistryTest.java +++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java @@ -5,54 +5,54 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -class LinkEnricherRegistryTest { +class HalEnricherRegistryTest { - private LinkEnricherRegistry registry; + private HalEnricherRegistry registry; @BeforeEach void setUpObjectUnderTest() { - registry = new LinkEnricherRegistry(); + registry = new HalEnricherRegistry(); } @Test void shouldRegisterTheEnricher() { - SampleLinkEnricher enricher = new SampleLinkEnricher(); + SampleHalEnricher enricher = new SampleHalEnricher(); registry.register(String.class, enricher); - Iterable enrichers = registry.allByType(String.class); + Iterable enrichers = registry.allByType(String.class); assertThat(enrichers).containsOnly(enricher); } @Test void shouldRegisterMultipleEnrichers() { - SampleLinkEnricher one = new SampleLinkEnricher(); + SampleHalEnricher one = new SampleHalEnricher(); registry.register(String.class, one); - SampleLinkEnricher two = new SampleLinkEnricher(); + SampleHalEnricher two = new SampleHalEnricher(); registry.register(String.class, two); - Iterable enrichers = registry.allByType(String.class); + Iterable enrichers = registry.allByType(String.class); assertThat(enrichers).containsOnly(one, two); } @Test void shouldRegisterEnrichersForDifferentTypes() { - SampleLinkEnricher one = new SampleLinkEnricher(); + SampleHalEnricher one = new SampleHalEnricher(); registry.register(String.class, one); - SampleLinkEnricher two = new SampleLinkEnricher(); + SampleHalEnricher two = new SampleHalEnricher(); registry.register(Integer.class, two); - Iterable enrichers = registry.allByType(String.class); + Iterable enrichers = registry.allByType(String.class); assertThat(enrichers).containsOnly(one); enrichers = registry.allByType(Integer.class); assertThat(enrichers).containsOnly(two); } - private static class SampleLinkEnricher implements LinkEnricher { + private static class SampleHalEnricher implements HalEnricher { @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { + public void enrich(HalEnricherContext context, HalAppender appender) { } } 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/de/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json index 0824a4ad38..37d6d4be2a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json @@ -1,9 +1,48 @@ { "scm-hg-plugin": { "information": { - "clone" : "Repository Klonen", - "create" : "Neue Repository erstellen", - "replace" : "Eine existierende Repository aktualisieren" + "clone" : "Repository klonen", + "create" : "Neues Repository erstellen", + "replace" : "Ein bestehendes Repository aktualisieren" + }, + "config": { + "link": "Mercurial", + "title": "Mercurial Konfiguration", + "hgBinary": "HG Binary", + "hgBinaryHelpText": "Pfad des Mercurial Binary.", + "pythonBinary": "Python Binary", + "pythonBinaryHelpText": "Pfad des Python binary.", + "pythonPath": "Python Module Such Pfad", + "pythonPathHelpText": "Python Module Such Pfad (PYTHONPATH).", + "encoding": "Encoding", + "encodingHelpText": "Repository Encoding.", + "useOptimizedBytecode": "Optimized Bytecode (.pyo)", + "useOptimizedBytecodeHelpText": "Verwende den Python '-O' Switch.", + "showRevisionInId": "Revision anzeigen", + "showRevisionInIdHelpText": "Die Revision als Teil der Node ID anzeigen.", + "enableHttpPostArgs": "HttpPostArgs Protocol aktivieren", + "enableHttpPostArgsHelpText": "Aktiviert das experimentelle HttpPostArgs Protokoll von Mercurial. Das HttpPostArgs Protokoll verwendet den Post Request Body anstatt des HTTP Headers um Meta Informationen zu versenden. Dieses Vorgehen reduziert die Header Größe der Mercurial Requests. HttpPostArgs wird seit Mercurial 3.8 unterstützt.", + "disableHookSSLValidation": "SSL Validierung für Hooks deaktivieren", + "disableHookSSLValidationHelpText": "Deaktiviert die Validierung von SSL Zertifikaten für den Mercurial Hook, der die Repositoryänderungen wieder zurück an den SCM-Manager leitet. Diese Option sollte nur benutzt werden, wenn der SCM-Manager ein selbstsigniertes Zertifikat verwendet.", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviert oder deaktiviert das Mercurial Plugin.", + "required": "Dieser Konfigurationswert wird benötigt" + } + }, + "permissions" : { + "configuration": { + "read": { + "hg": { + "displayName": "Mercurial Konfiguration lesen", + "description": "Darf die Mercurial Konfiguration lesen" + } + }, + "write": { + "hg": { + "displayName": "Mercurial Konfiguration schreiben", + "description": "Darf die Mercurial Konfiguration verändern" + } + } } } } 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..61340ab9cf 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": "Enables the experimental HttpPostArgs Protocol of mercurial. The HttpPostArgs Protocol uses the body of post requests to send the meta information instead of http headers. This helps to reduce the header size of mercurial requests. HttpPostArgs is supported since mercurial 3.8.", + "disableHookSSLValidation": "Disable SSL Validation on Hooks", + "disableHookSSLValidationHelpText": "Disables the validation of ssl certificates for the mercurial hook, which forwards the repository changes back to scm-manager. This option should only be used, if SCM-Manager uses a self signed certificate.", "disabled": "Disabled", "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-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java index bbcaa9a9d5..580bc0b77d 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java @@ -21,41 +21,51 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif super(context, repository); } - @Override - @SuppressWarnings("unchecked") - public Modifications getModifications(String revision) { - final Modifications modifications = new Modifications(); - log.debug("get modifications {}", revision); + public Modifications getModifications(String revisionOrTransactionId) { + Modifications modifications; try { - if (SvnUtil.isTransactionEntryId(revision)) { - - SVNLookClient client = SVNClientManager.newInstance().getLookClient(); - client.doGetChanged(context.getDirectory(), SvnUtil.getTransactionId(revision), - e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true); - - return modifications; - + if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) { + modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId)); } else { - - long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); - SVNRepository repo = open(); - Collection entries = repo.log(null, null, revisionNumber, - revisionNumber, true, true); - if (Util.isNotEmpty(entries)) { - return SvnUtil.createModifications(entries.iterator().next(), revision); - } + modifications = getModificationFromRevision(revisionOrTransactionId); } + return modifications; } catch (SVNException ex) { - throw new InternalRepositoryException(repository, "could not open repository", ex); + throw new InternalRepositoryException( + repository, + "failed to get svn modifications for " + revisionOrTransactionId, + ex + ); + } + } + + @SuppressWarnings("unchecked") + private Modifications getModificationFromRevision(String revision) throws SVNException { + log.debug("get svn modifications from revision: {}", revision); + long revisionNumber = SvnUtil.getRevisionNumber(revision, repository); + SVNRepository repo = open(); + Collection entries = repo.log(null, null, revisionNumber, + revisionNumber, true, true); + if (Util.isNotEmpty(entries)) { + return SvnUtil.createModifications(entries.iterator().next(), revision); } return null; } + private Modifications getModificationsFromTransaction(String transaction) throws SVNException { + log.debug("get svn modifications from transaction: {}", transaction); + final Modifications modifications = new Modifications(); + SVNLookClient client = SVNClientManager.newInstance().getLookClient(); + client.doGetChanged(context.getDirectory(), transaction, + e -> SvnUtil.appendModification(modifications, e.getType(), e.getPath()), true); + + return modifications; + } + @Override public Modifications getModifications(ModificationsCommandRequest request) { return getModifications(request.getRevision()); } - } diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json index 7c58498ef1..58a18482b2 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/de/plugins.json @@ -1,7 +1,42 @@ { "scm-svn-plugin": { "information": { - "checkout" : "Repository auschecken" + "checkout": "Repository auschecken" + }, + "config": { + "link": "Subversion", + "title": "Subversion Konfiguration", + "compatibility": "Version Kompatibilität", + "compatibilityHelpText": "Gibt an, mit welcher Subversion Version die Repositories kompatibel sind.", + "compatibility-values": { + "none": "Keine Kompatibilität", + "pre14": "Vor 1.4 kompatibel", + "pre15": "Vor 1.5 kompatibel", + "pre16": "Vor 1.6 kompatibel", + "pre17": "Vor 1.7 kompatibel", + "with17": "Mit 1.7 kompatibel" + }, + "enabledGZip": "GZip Compression aktivieren", + "enabledGZipHelpText": "Aktiviert GZip Kompression für SVN Responses", + "disabled": "Deaktiviert", + "disabledHelpText": "Aktiviert oder deaktiviert das SVN Plugin", + "required": "Dieser Konfigurationswert wird benötigt" + } + }, + "permissions": { + "configuration": { + "read": { + "svn": { + "displayName": "Subversion Konfiguration lesen", + "description": "Darf die Subversion Konfiguration lesen" + } + }, + "write": { + "svn": { + "displayName": "Subversion Konfiguration schreiben", + "description": "Darf die Subversion Konfiguration verändern" + } + } } } } diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json index 2a363c77cd..a796027afc 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json @@ -7,7 +7,7 @@ "link": "Subversion", "title": "Subversion Configuration", "compatibility": "Version Compatibility", - "compatibilityHelpText": "Specifies with which subversion version repositories are compatible.", + "compatibilityHelpText": "Specifies with which Subversion version repositories are compatible.", "compatibility-values": { "none": "No compatibility", "pre14": "Pre 1.4 Compatible", @@ -17,9 +17,9 @@ "with17": "With 1.7 Compatible" }, "enabledGZip": "Enable GZip Compression", - "enabledGZipHelpText": "Enable GZip compression for svn responses.", + "enabledGZipHelpText": "Enable GZip compression for SVN responses.", "disabled": "Disabled", - "disabledHelpText": "Enable or disable the Git plugin", + "disabledHelpText": "Enable or disable the SVN plugin", "required": "This configuration value is required" } }, 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/config/ConfigurationBinder.js b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js index 1b2b37bb19..96e8c630b8 100644 --- a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js +++ b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js @@ -36,9 +36,9 @@ class ConfigurationBinder { binder.bind("config.navigation", ConfigNavLink, configPredicate); // route for global configuration, passes the link from the index resource to component - const ConfigRoute = ({ url, links }) => { + const ConfigRoute = ({ url, links, ...additionalProps }) => { const link = links[linkName].href; - return this.route(url + to, ); + return this.route(url + to, ); }; // bind config route to extension point @@ -63,9 +63,9 @@ class ConfigurationBinder { // route for global configuration, passes the current repository to component - const RepoRoute = ({url, repository}) => { - const link = repository._links[linkName].href - return this.route(url + to, ); + const RepoRoute = ({url, repository, ...additionalProps}) => { + const link = repository._links[linkName].href; + return this.route(url + to, ); }; // bind config route to extension point diff --git a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js index e5c04eb613..013014cd98 100644 --- a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js +++ b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js @@ -48,7 +48,7 @@ class AddEntryToTableField extends React.Component { ); 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..c38dff2468 --- /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": "Klartext Passwort des Benutzers.", + "passwordConfirmHelpText": "Passwort zur Bestätigen wiederholen.", + "currentPassword": "Aktuelles Passwort", + "currentPasswordHelpText": "Dieses Passwort wird momentan bereits verwendet.", + "confirmPassword": "Passwort wiederholen", + "passwordInvalid": "Das Passwort muss zwischen 6 und 32 Zeichen lang sein!", + "passwordConfirmFailed": "Passwörter müssen identisch sein!", + "submit": "Speichern", + "changedSuccessfully": "Passwort erfolgreich geändert!" + } +} diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json new file mode 100644 index 0000000000..e26c2cddac --- /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 Anmeldeversuchen" + }, + "general-settings": { + "realm-description": "Realm Beschreibung", + "enable-repository-archive": "Repository Archiv aktivieren", + "disable-grouping-grid": "Gruppen deaktivieren", + "date-format": "Datumsformat", + "anonymous-access-enabled": "Anonyme Zugriffe erlauben", + "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", + "plugin-url": "Plugin URL", + "enabled-xsrf-protection": "XSRF Protection aktivieren", + "default-namespace-strategy": "Default Namespace Strategie" + }, + "validation": { + "date-format-invalid": "Das Datumsformat ist ungültig", + "login-attempt-limit-timeout-invalid": "Dies ist keine Zahl", + "login-attempt-limit-invalid": "Dies ist keine Zahl", + "plugin-url-invalid": "Dies ist keine gültige URL" + }, + "help": { + "realmDescriptionHelpText": "Beschreibung des Authentication Realm.", + "dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.", + "pluginRepositoryHelpText": "Die URL des Plugin Repositories. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur", + "enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.", + "enableRepositoryArchiveHelpText": "Repository Archive aktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", + "disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", + "allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf öffentliche Repositories.", + "skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.", + "adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.", + "adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.", + "forceBaseUrlHelpText": "Zugriffe, die von einer anderen URL kommen, werden auf die Base URL weiter geleitet.", + "baseUrlHelpText": "Die URL der Applikation mit Kontextpfad, z.B. http://localhost:8080/scm", + "loginAttemptLimitHelpText": "Maximale Anzahl von Anmeldeversuchen. Durch Verwendung von -1 wird die Begrenzung der Anmeldeversuche deaktiviert.", + "loginAttemptLimitTimeoutHelpText": "Timeout in Sekunden für Benutzer, die vorübergehend wegen zu vieler fehlgeschlagener Anmeldeversuche, deaktiviert wurden.", + "enableProxyHelpText": "Proxy aktivieren", + "proxyPortHelpText": "Der Proxy Port", + "proxyPasswordHelpText": "Das Passwort für die Proxy Server Anmeldung.", + "proxyServerHelpText": "Der Proxy Server", + "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", + "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", + "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", + "defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces." + } +} diff --git a/scm-ui/public/locales/de/groups.json b/scm-ui/public/locales/de/groups.json new file mode 100644 index 0000000000..768704bf38 --- /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": "Erstellen einer neuen Gruppe" + }, + "create-group-button": { + "label": "Gruppe 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": "Eindeutiger 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..e82edf7512 --- /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 Repository 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": "Repository 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 Repository" + }, + "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 Repository. Dieser wird Teil der URL des Repository sein.", + "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", + "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", + "descriptionHelpText": "Eine kurze Beschreibung des Repository." + } +} diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json new file mode 100644 index 0000000000..31b954d996 --- /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": "Erstellt", + "lastModified": "Zuletzt bearbeitet" + }, + "users": { + "title": "Benutzer", + "subtitle": "Verwaltung der Benutzer" + }, + "create-user-button": { + "label": "Benutzer 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..4788f7b39f 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -1,7 +1,7 @@ { "login": { "title": "Login", - "subtitle": "Please login to proceed.", + "subtitle": "Please login to proceed", "logo-alt": "SCM-Manager", "username-placeholder": "Your Username", "password-placeholder": "Your Password", @@ -22,7 +22,7 @@ "error-notification": { "prefix": "Error", "loginLink": "You can login here again.", - "timeout": "The session has expired.", + "timeout": "The session has expired", "wrong-login-credentials": "Invalid credentials" }, "loading": { @@ -50,7 +50,7 @@ "mail": "E-Mail", "groups": "Groups", "information": "Information", - "change-password": "Change password", + "change-password": "Change Password", "error-title": "Error", "error-subtitle": "Cannot display profile", "error": "Error", @@ -59,14 +59,14 @@ "password": { "label": "Password", "newPassword": "New password", - "passwordHelpText": "Plain text password of the user.", - "passwordConfirmHelpText": "Repeat the password for confirmation.", + "passwordHelpText": "Plain text password of the user", + "passwordConfirmHelpText": "Repeat the password for confirmation", "currentPassword": "Current password", "currentPasswordHelpText": "The password currently in use", "confirmPassword": "Confirm password", "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/config.json b/scm-ui/public/locales/en/config.json index 1a33da8c8b..bdc7ec21f6 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -67,17 +67,17 @@ "plugin-url-invalid": "This is not a valid url" }, "help": { - "realmDescriptionHelpText": "Enter authentication realm description", - "dateFormatHelpText": "Moments date format. Please have a look at the momentjs documentation.", + "realmDescriptionHelpText": "Enter authentication realm description.", + "dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.", "pluginRepositoryHelpText": "The url of the plugin repository. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture", - "enableForwardingHelpText": "Enbale mod_proxy port forwarding.", + "enableForwardingHelpText": "Enable mod_proxy port forwarding.", "enableRepositoryArchiveHelpText": "Enable repository archives. A complete page reload is required after a change of this value.", "disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.", "allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.", "skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.", "adminGroupsHelpText": "Names of groups with admin permissions.", "adminUsersHelpText": "Names of users with admin permissions.", - "forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url", + "forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url.", "baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm", "loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.", "loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.", @@ -86,8 +86,8 @@ "proxyPasswordHelpText": "The password for the proxy server authentication.", "proxyServerHelpText": "The proxy server", "proxyUserHelpText": "The username for the proxy server authentication.", - "proxyExcludesHelpText": "Glob patterns for hostnames which should be excluded from proxy settings.", - "enableXsrfProtectionHelpText": "Enable Xsrf Cookie Protection. Note: This feature is still experimental.", - "defaultNameSpaceStrategyHelpText": "The default namespace strategy" + "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", + "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", + "defaultNameSpaceStrategyHelpText": "The default namespace strategy." } } diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index 3fbe088029..60a10e4302 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -24,23 +24,23 @@ "subtitle": "Create a new group" }, "create-group-button": { - "label": "Create" + "label": "Create Group" }, "edit-group-button": { "label": "Edit" }, "add-member-button": { - "label": "Add member" + "label": "Add Member" }, "remove-member-button": { - "label": "Remove member" + "label": "Remove Member" }, "add-member-textfield": { - "label": "Add member", + "label": "Add Member", "error": "Invalid member name" }, "add-member-autocomplete": { - "placeholder": "Enter member", + "placeholder": "Enter Member", "loading": "Loading...", "no-options": "No suggestion available" }, @@ -65,6 +65,6 @@ } }, "set-permissions-button": { - "label": "Set permissions" + "label": "Set Permissions" } } diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json index 52059db60a..f5ba065ced 100644 --- a/scm-ui/public/locales/en/permissions.json +++ b/scm-ui/public/locales/en/permissions.json @@ -1,7 +1,7 @@ { "form": { "submit-button": { - "label": "Set permissions" + "label": "Set Permissions" }, "set-permissions-successful": "Permissions set successfully" } diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index d6cdaa1d8d..727fe4f664 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -14,7 +14,7 @@ "overview": { "title": "Repositories", "subtitle": "Overview of available repositories", - "create-button": "Create" + "create-button": "Create Repository" }, "repository-root": { "error-title": "Error", @@ -40,7 +40,7 @@ "delete-nav-action": { "label": "Delete", "confirm-alert": { - "title": "Delete repository", + "title": "Delete Repository", "message": "Do you really want to delete the repository?", "submit": "Yes", "cancel": "No" @@ -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/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index afe86deb9b..4c671617ec 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -15,12 +15,12 @@ "subtitle": "Create, read, update and delete users" }, "create-user-button": { - "label": "Create" + "label": "Create User" }, "delete-user-button": { "label": "Delete", "confirm-alert": { - "title": "Delete user", + "title": "Delete User", "message": "Do you really want to delete the user?", "submit": "Yes", "cancel": "No" @@ -30,10 +30,10 @@ "label": "Edit" }, "set-password-button": { - "label": "Set password" + "label": "Set Password" }, "set-permissions-button": { - "label": "Set permissions" + "label": "Set Permissions" }, "user-form": { "submit": "Submit" @@ -63,6 +63,6 @@ "displayNameHelpText": "Display name of the user.", "mailHelpText": "Email address of the user.", "adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.", - "activeHelpText": "Activate or deactive the user." + "activeHelpText": "Activate or deactivate the user." } } diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 07b6681752..4ab9382cc1 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -21,7 +21,7 @@ import ChangesetView from "./ChangesetView"; import PermissionsNavLink from "../components/PermissionsNavLink"; import Sources from "../sources/containers/Sources"; import RepositoryNavLink from "../components/RepositoryNavLink"; -import {getRepositoriesLink} from "../../modules/indexResource"; +import {getLinks, getRepositoriesLink} from "../../modules/indexResource"; import {ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { @@ -31,6 +31,7 @@ type Props = { loading: boolean, error: Error, repoLink: string, + indexLinks: Object, // dispatch functions fetchRepoByName: (link: string, namespace: string, name: string) => void, @@ -75,7 +76,7 @@ class RepositoryRoot extends React.Component { }; render() { - const { loading, error, repository, t } = this.props; + const { loading, error, indexLinks, repository, t } = this.props; if (error) { return ( @@ -95,7 +96,8 @@ class RepositoryRoot extends React.Component { const extensionProps = { repository, - url + url, + indexLinks }; return ( @@ -216,13 +218,15 @@ const mapStateToProps = (state, ownProps) => { const loading = isFetchRepoPending(state, namespace, name); const error = getFetchRepoFailure(state, namespace, name); const repoLink = getRepositoriesLink(state); + const indexLinks = getLinks(state); return { namespace, name, repository, loading, error, - repoLink + repoLink, + indexLinks }; }; 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/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index ba1e2f6753..9bf36d19d0 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -61,16 +61,42 @@ class UserForm extends React.Component { return false; } + createUserComponentsAreInvalid = () => { + const user = this.state.user; + if (!this.props.user) { + return ( + this.state.nameValidationError || + this.isFalsy(user.name) || + !this.state.passwordValid + ); + } else { + return false; + } + }; + + editUserComponentsAreUnchanged = () => { + const user = this.state.user; + if (this.props.user) { + return ( + this.props.user.displayName === user.displayName && + this.props.user.mail === user.mail && + this.props.user.admin === user.admin && + this.props.user.active === user.active + ); + } else { + return false; + } + }; + isValid = () => { const user = this.state.user; return !( - this.state.nameValidationError || + this.createUserComponentsAreInvalid() || + this.editUserComponentsAreUnchanged() || this.state.mailValidationError || this.state.displayNameValidationError || - this.isFalsy(user.name) || this.isFalsy(user.displayName) || - this.isFalsy(user.mail) || - !this.state.passwordValid + this.isFalsy(user.mail) ); }; 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/BranchDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java index f5bdc850ab..343d9c8bc8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -12,8 +13,7 @@ public class BranchDto extends HalRepresentation { private String name; private String revision; - @Override - protected HalRepresentation add(Links links) { - return super.add(links); + BranchDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index 7e6f0c074c..c940b1ffd9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -1,11 +1,11 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.Branch; import sonia.scm.repository.NamespaceAndName; @@ -15,7 +15,7 @@ import static de.otto.edison.hal.Link.linkBuilder; import static de.otto.edison.hal.Links.linkingTo; @Mapper -public abstract class BranchToBranchDtoMapper extends LinkAppenderMapper { +public abstract class BranchToBranchDtoMapper extends HalAppenderMapper { @Inject private ResourceLinks resourceLinks; @@ -23,16 +23,17 @@ public abstract class BranchToBranchDtoMapper extends LinkAppenderMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName); - @AfterMapping - void appendLinks(Branch source, @MappingTarget BranchDto target, @Context NamespaceAndName namespaceAndName) { + @ObjectFactory + BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) { Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.branch().self(namespaceAndName, target.getName())) - .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build()) - .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()) - .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()); + .self(resourceLinks.branch().self(namespaceAndName, branch.getName())) + .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, branch.getName())).build()) + .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()) + .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()); - appendLinks(new EdisonLinkAppender(linksBuilder), source, namespaceAndName); + Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName); - target.add(linksBuilder.build()); + return new BranchDto(linksBuilder.build(), embeddedBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index 4c9620564b..f77823eaac 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -23,7 +23,9 @@ public class ConfigDto extends HalRepresentation { private boolean disableGroupingGrid; private String dateFormat; private boolean anonymousAccessEnabled; + @NoBlankStrings private Set adminGroups; + @NoBlankStrings private Set adminUsers; private String baseUrl; private boolean forceBaseUrl; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index bf3a11fb9c..c646dceab4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -9,6 +9,7 @@ import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -71,7 +72,7 @@ public class ConfigResource { @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(ConfigDto configDto) { + public Response update(@Valid ConfigDto configDto) { // This *could* be moved to ScmConfiguration or ScmConfigurationUtil classes. // But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonLinkAppender.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java similarity index 52% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonLinkAppender.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java index c4e699cb58..769de2b705 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonLinkAppender.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/EdisonHalAppender.java @@ -1,27 +1,36 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import java.util.ArrayList; import java.util.List; -class EdisonLinkAppender implements LinkAppender { +class EdisonHalAppender implements HalAppender { - private final Links.Builder builder; + private final Links.Builder linkBuilder; + private final Embedded.Builder embeddedBuilder; - EdisonLinkAppender(Links.Builder builder) { - this.builder = builder; + EdisonHalAppender(Links.Builder linkBuilder, Embedded.Builder embeddedBuilder) { + this.linkBuilder = linkBuilder; + this.embeddedBuilder = embeddedBuilder; } @Override - public void appendOne(String rel, String href) { - builder.single(Link.link(rel, href)); + public void appendLink(String rel, String href) { + linkBuilder.single(Link.link(rel, href)); } @Override - public LinkArrayBuilder arrayBuilder(String rel) { - return new EdisonLinkArrayBuilder(builder, rel); + public LinkArrayBuilder linkArrayBuilder(String rel) { + return new EdisonLinkArrayBuilder(linkBuilder, rel); + } + + @Override + public void appendEmbedded(String rel, HalRepresentation embedded) { + embeddedBuilder.with(rel, embedded); } private static class EdisonLinkArrayBuilder implements LinkArrayBuilder { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java index c183d731c6..0bce564e35 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -27,10 +28,8 @@ public class FileObjectDto extends HalRepresentation { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String revision; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + public FileObjectDto(Links links, Embedded embedded) { + super(links, embedded); } public void setChildren(List children) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java index 2432d5168c..608dea9f26 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -1,24 +1,22 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.SubRepository; import javax.inject.Inject; -import java.util.List; -import java.util.stream.Collectors; - +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; @Mapper -public abstract class FileObjectToFileObjectDtoMapper extends LinkAppenderMapper implements InstantAttributeMapper { +public abstract class FileObjectToFileObjectDtoMapper extends HalAppenderMapper implements InstantAttributeMapper { @Inject private ResourceLinks resourceLinks; @@ -28,20 +26,21 @@ public abstract class FileObjectToFileObjectDtoMapper extends LinkAppenderMapper abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); - @AfterMapping - void addLinks(FileObject fileObject, @MappingTarget FileObjectDto dto, @Context NamespaceAndName namespaceAndName, @Context String revision) { + @ObjectFactory + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context String revision, FileObject fileObject) { String path = removeFirstSlash(fileObject.getPath()); Links.Builder links = Links.linkingTo(); - if (dto.isDirectory()) { + if (fileObject.isDirectory()) { links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path)); } else { links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path)); links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path))); } - appendLinks(new EdisonLinkAppender(links), fileObject, namespaceAndName, revision); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(links, embeddedBuilder), fileObject, namespaceAndName, revision); - dto.add(links.build()); + return new FileObjectDto(links.build(), embeddedBuilder.build()); } private String removeFirstSlash(String source) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java index 760beab1da..2ccfcff38e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -28,13 +29,7 @@ public class GroupDto extends HalRepresentation { private Map properties; private List members; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); - } - - public HalRepresentation withMembers(List members) { - return super.withEmbedded("members", members); + GroupDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java index bf866af350..7d5ddae548 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java @@ -1,9 +1,9 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.group.Group; import sonia.scm.group.GroupPermissions; import sonia.scm.security.PermissionPermissions; @@ -12,6 +12,7 @@ import javax.inject.Inject; import java.util.List; import java.util.stream.Collectors; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -23,28 +24,26 @@ public abstract class GroupToGroupDtoMapper extends BaseMapper @Inject private ResourceLinks resourceLinks; - @AfterMapping - void appendLinks(Group group, @MappingTarget GroupDto target) { - Links.Builder linksBuilder = linkingTo().self(resourceLinks.group().self(target.getName())); + @ObjectFactory + GroupDto createDto(Group group) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.group().self(group.getName())); if (GroupPermissions.delete(group).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.group().delete(target.getName()))); + linksBuilder.single(link("delete", resourceLinks.group().delete(group.getName()))); } if (GroupPermissions.modify(group).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.group().update(target.getName()))); + linksBuilder.single(link("update", resourceLinks.group().update(group.getName()))); } if (PermissionPermissions.read().isPermitted()) { - linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(group.getName()))); } - appendLinks(new EdisonLinkAppender(linksBuilder), group); - - target.add(linksBuilder.build()); - } - - @AfterMapping - void mapMembers(Group group, @MappingTarget GroupDto target) { + Embedded.Builder embeddedBuilder = embeddedBuilder(); List memberDtos = group.getMembers().stream().map(this::createMember).collect(Collectors.toList()); - target.withMembers(memberDtos); + embeddedBuilder.with("members", memberDtos); + + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), group); + + return new GroupDto(linksBuilder.build(), embeddedBuilder.build()); } private MemberDto createMember(String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java index 9346420f58..16f945332d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -9,8 +10,8 @@ public class IndexDto extends HalRepresentation { private final String version; - IndexDto(String version, Links links) { - super(links); + IndexDto(Links links, Embedded embedded, String version) { + super(links, embedded); this.version = version; } } 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..90445bcdc2 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 @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.Lists; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; @@ -13,9 +14,10 @@ import sonia.scm.user.UserPermissions; import javax.inject.Inject; import java.util.List; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; -public class IndexDtoGenerator extends LinkAppenderMapper { +public class IndexDtoGenerator extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final SCMContextProvider scmContextProvider; @@ -56,12 +58,14 @@ 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())); } - appendLinks(new EdisonLinkAppender(builder), new Index()); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(builder, embeddedBuilder), new Index()); - return new IndexDto(scmContextProvider.getVersion(), builder.build()); + return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java index 890e268ed5..8472eb9fc1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistration.java @@ -10,30 +10,30 @@ import javax.servlet.ServletContextListener; import java.util.Set; /** - * Registers every {@link LinkEnricher} which is annotated with an {@link Enrich} annotation. + * Registers every {@link HalEnricher} which is annotated with an {@link Enrich} annotation. */ @Extension public class LinkEnricherAutoRegistration implements ServletContextListener { private static final Logger LOG = LoggerFactory.getLogger(LinkEnricherAutoRegistration.class); - private final LinkEnricherRegistry registry; - private final Set enrichers; + private final HalEnricherRegistry registry; + private final Set enrichers; @Inject - public LinkEnricherAutoRegistration(LinkEnricherRegistry registry, Set enrichers) { + public LinkEnricherAutoRegistration(HalEnricherRegistry registry, Set enrichers) { this.registry = registry; this.enrichers = enrichers; } @Override public void contextInitialized(ServletContextEvent sce) { - for (LinkEnricher enricher : enrichers) { + for (HalEnricher enricher : enrichers) { Enrich annotation = enricher.getClass().getAnnotation(Enrich.class); if (annotation != null) { registry.register(annotation.value(), enricher); } else { - LOG.warn("found LinkEnricher extension {} without Enrich annotation", enricher.getClass()); + LOG.warn("found HalEnricher extension {} without Enrich annotation", enricher.getClass()); } } } 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 e5142dc9fa..c74f16ad70 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(DefaultChangesetToChangesetDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java index 5488faca28..84fbbfe290 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -18,9 +19,7 @@ public class MeDto extends HalRepresentation { private String mail; private List groups; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + MeDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java index 082db7fd94..b5e1998066 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.ImmutableList; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; @@ -13,10 +14,11 @@ import sonia.scm.user.UserPermissions; import javax.inject.Inject; import java.util.Collections; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; -public class MeDtoFactory extends LinkAppenderMapper { +public class MeDtoFactory extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final UserManager userManager; @@ -29,15 +31,11 @@ public class MeDtoFactory extends LinkAppenderMapper { public MeDto create() { PrincipalCollection principals = getPrincipalCollection(); - - MeDto dto = new MeDto(); - User user = principals.oneByType(User.class); + MeDto dto = createDto(user); mapUserProperties(user, dto); mapGroups(principals, dto); - - appendLinks(user, dto); return dto; } @@ -61,21 +59,22 @@ public class MeDtoFactory extends LinkAppenderMapper { } - private void appendLinks(User user, MeDto target) { + private MeDto createDto(User user) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); if (UserPermissions.delete(user).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName()))); + linksBuilder.single(link("delete", resourceLinks.me().delete(user.getName()))); } if (UserPermissions.modify(user).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.me().update(target.getName()))); + linksBuilder.single(link("update", resourceLinks.me().update(user.getName()))); } if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) { linksBuilder.single(link("password", resourceLinks.me().passwordChange())); } - appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), new Me(), user); - target.add(linksBuilder.build()); + return new MeDto(linksBuilder.build(), embeddedBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStrings.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStrings.java new file mode 100644 index 0000000000..ba5e20ffbd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStrings.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Constraint(validatedBy = NoBlankStringsValidator.class) +@Documented +public @interface NoBlankStrings { + + String message() default "collection must not contain empty strings"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStringsValidator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStringsValidator.java new file mode 100644 index 0000000000..6bae44164e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NoBlankStringsValidator.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Collection; + +public class NoBlankStringsValidator implements ConstraintValidator { + + @Override + public void initialize(NoBlankStrings constraintAnnotation) { + } + + @Override + public boolean isValid(Collection object, ConstraintValidatorContext constraintContext) { + if ( object == null || object.isEmpty()) { + return true; + } + return object.stream() + .map(x -> x.toString()) + .map(s -> ((String) s).trim()) + .noneMatch(s -> ((String) s).isEmpty()); + } +} 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/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index ddfe432d73..8b48311bba 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -1,9 +1,11 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; @@ -13,7 +15,7 @@ import java.time.Instant; import java.util.List; import java.util.Map; -@Getter @Setter +@Getter @Setter @NoArgsConstructor public class RepositoryDto extends HalRepresentation { @Email @@ -31,9 +33,7 @@ public class RepositoryDto extends HalRepresentation { private String type; protected Map properties; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + RepositoryDto(Links links, Embedded embedded) { + super(links, embedded); } } 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/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 19929b63ba..9e680b7e5c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -1,11 +1,11 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.repository.Feature; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Repository; @@ -17,6 +17,7 @@ import sonia.scm.repository.api.ScmProtocol; import java.util.List; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; import static java.util.stream.Collectors.toList; @@ -33,17 +34,17 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper properties; - @Override - @SuppressWarnings("squid:S1185") // We want to have this method available in this package - protected HalRepresentation add(Links links) { - return super.add(links); + UserDto(Links links, Embedded embedded) { + super(links, embedded); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java index 3c7e9fd7f1..ac641e3e66 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java @@ -1,10 +1,10 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; +import org.mapstruct.ObjectFactory; import sonia.scm.security.PermissionPermissions; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -12,6 +12,7 @@ import sonia.scm.user.UserPermissions; import javax.inject.Inject; +import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -31,25 +32,26 @@ public abstract class UserToUserDtoMapper extends BaseMapper { @Inject private ResourceLinks resourceLinks; - @AfterMapping - protected void appendLinks(User user, @MappingTarget UserDto target) { - Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName())); + @ObjectFactory + UserDto createDto(User user) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(user.getName())); if (UserPermissions.delete(user).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.user().delete(target.getName()))); + linksBuilder.single(link("delete", resourceLinks.user().delete(user.getName()))); } if (UserPermissions.modify(user).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.user().update(target.getName()))); + linksBuilder.single(link("update", resourceLinks.user().update(user.getName()))); if (userManager.isTypeDefault(user)) { - linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName()))); + linksBuilder.single(link("password", resourceLinks.user().passwordChange(user.getName()))); } } if (PermissionPermissions.read().isPermitted()) { - linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(user.getName()))); } - appendLinks(new EdisonLinkAppender(linksBuilder), user); + Embedded.Builder embeddedBuilder = embeddedBuilder(); + applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), user); - target.add(linksBuilder.build()); + return new UserDto(linksBuilder.build(), embeddedBuilder.build()); } } 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/i18n/I18nServlet.java b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java index b074781fec..08a4a33493 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java @@ -78,8 +78,9 @@ public class I18nServlet extends HttpServlet { @VisibleForTesting @Override protected void doGet(HttpServletRequest req, HttpServletResponse response) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); try (PrintWriter out = response.getWriter()) { - response.setContentType("application/json"); String path = req.getServletPath(); Function> jsonFileProvider = usedPath -> Optional.empty(); BiConsumer createdJsonFileConsumer = (usedPath, jsonNode) -> log.debug("A json File is created from the path {}", usedPath); 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/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json new file mode 100644 index 0000000000..96eb8e8e9b --- /dev/null +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -0,0 +1,81 @@ +{ + "permissions": { + "repository": { + "read,pull": { + "*": { + "displayName": "Alle Repositories lesen", + "description": "Darf alle Repositories lesen und klonen." + } + }, + "read,pull,push": { + "*": { + "displayName": "Alle Repositories schreiben", + "description": "Darf alle Repositories lesen, klonen und schreiben." + } + }, + "*": { + "*": { + "displayName": "Alle Repositories besitzen (Owner)", + "description": "Darf alle Repositories lesen, klonen, schreiben, konfigurieren und löschen." + } + }, + "create": { + "displayName": "Repositories erstellen", + "description": "Darf Repositories erstellen." + } + }, + "user": { + "*": { + "displayName": "Benutzer administrieren", + "description": "Darf Benutzer administrieren." + } + }, + "group": { + "*": { + "displayName": "Gruppen administrieren", + "description": "Darf Gruppen administrieren." + } + }, + "unknown": "Unbekannte Berechtigung" + }, + "verbs": { + "repository": { + "read": { + "displayName": "Lesen", + "description": "Darf das Repository im SCM-Manager sehen." + }, + "modify": { + "displayName": "Modifizieren", + "description": "Darf die Eigenschaften des Repository verändern." + }, + "delete": { + "displayName": "Löschen", + "description": "Darf das Repository löschen." + }, + "pull": { + "displayName": "Pull/Checkout", + "description": "Darf pull/checkout auf das Repository ausführen." + }, + "push": { + "displayName": "Push/Commit", + "description": "Darf push/commit auf das Repository ausführen und damit den Inhalt verändern." + }, + "permissionRead": { + "displayName": "Berechtigungen lesen", + "description": "Darf die Berechtigungen des Repository sehen." + }, + "permissionWrite": { + "displayName": "Berechtigungen modifizieren", + "description": "Darf die Berechtigungen des Repository bearbeiten." + }, + "healthCheck": { + "displayName": "Health Check", + "description": "Darf den Repository Health Check ausführen." + }, + "*": { + "displayName": "Alle Repository Rechte", + "description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen." + } + } + } +} 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/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java index d2e202576a..3e64ab95b6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java @@ -24,12 +24,12 @@ class BranchToBranchDtoMapperTest { @Test void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Branch.class, (ctx, appender) -> { NamespaceAndName namespaceAndName = ctx.oneRequireByType(NamespaceAndName.class); Branch branch = ctx.oneRequireByType(Branch.class); - appender.appendOne("ka", "http://" + namespaceAndName.logString() + "/" + branch.getName()); + appender.appendLink("ka", "http://" + namespaceAndName.logString() + "/" + branch.getName()); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 033824cbea..4fb25d2371 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -92,11 +92,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readWrite") public void shouldUpdateConfig() throws URISyntaxException, IOException { - URL url = Resources.getResource("sonia/scm/api/v2/config-test-update.json"); - byte[] configJson = Resources.toByteArray(url); - MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) - .contentType(VndMediaType.CONFIG) - .content(configJson); + MockHttpRequest request = post("sonia/scm/api/v2/config-test-update.json"); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -113,11 +109,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readOnly") public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { - URL url = Resources.getResource("sonia/scm/api/v2/config-test-update.json"); - byte[] configJson = Resources.toByteArray(url); - MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) - .contentType(VndMediaType.CONFIG) - .content(configJson); + MockHttpRequest request = post("sonia/scm/api/v2/config-test-update.json"); MockHttpResponse response = new MockHttpResponse(); thrown.expectMessage("Subject does not have permission [configuration:write:global]"); @@ -125,6 +117,36 @@ public class ConfigResourceTest { dispatcher.invoke(request, response); } + @Test + @SubjectAware(username = "readWrite") + public void shouldFailForEmptyAdminUsers() throws URISyntaxException, IOException { + MockHttpRequest request = post("sonia/scm/api/v2/config-test-empty-admin-user.json"); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldFailForEmptyAdminGroups() throws URISyntaxException, IOException { + MockHttpRequest request = post("sonia/scm/api/v2/config-test-empty-admin-group.json"); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + private MockHttpRequest post(String resourceName) throws IOException, URISyntaxException { + URL url = Resources.getResource(resourceName); + byte[] configJson = Resources.toByteArray(url); + return MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) + .contentType(VndMediaType.CONFIG) + .content(configJson); + } + private static ScmConfiguration createConfiguration() { ScmConfiguration scmConfiguration = new ScmConfiguration(); scmConfiguration.setProxyPassword("heartOfGold"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonHalAppenderTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonHalAppenderTest.java new file mode 100644 index 0000000000..ff149c5dc5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonHalAppenderTest.java @@ -0,0 +1,61 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Link; +import de.otto.edison.hal.Links; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Links.linkingTo; +import static org.assertj.core.api.Assertions.assertThat; + +class EdisonHalAppenderTest { + + private Links.Builder linksBuilder; + private Embedded.Builder embeddedBuilder; + private EdisonHalAppender appender; + + @BeforeEach + void prepare() { + linksBuilder = linkingTo(); + embeddedBuilder = embeddedBuilder(); + appender = new EdisonHalAppender(linksBuilder, embeddedBuilder); + } + + @Test + void shouldAppendOneLink() { + appender.appendLink("self", "https://scm.hitchhiker.com"); + + Links links = linksBuilder.build(); + assertThat(links.getLinkBy("self").get().getHref()).isEqualTo("https://scm.hitchhiker.com"); + } + + @Test + void shouldAppendMultipleLinks() { + appender.linkArrayBuilder("items") + .append("one", "http://one") + .append("two", "http://two") + .build(); + + List items = linksBuilder.build().getLinksBy("items"); + assertThat(items).hasSize(2); + } + + @Test + void shouldAppendEmbedded() { + HalRepresentation one = new HalRepresentation(); + appender.appendEmbedded("one", one); + + HalRepresentation two = new HalRepresentation(); + appender.appendEmbedded("two", new HalRepresentation()); + + Embedded embedded = embeddedBuilder.build(); + assertThat(embedded.getItemsBy("one")).containsOnly(one); + assertThat(embedded.getItemsBy("two")).containsOnly(two); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonLinkAppenderTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonLinkAppenderTest.java deleted file mode 100644 index e97415cc09..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/EdisonLinkAppenderTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package sonia.scm.api.v2.resources; - -import de.otto.edison.hal.Link; -import de.otto.edison.hal.Links; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static de.otto.edison.hal.Links.linkingTo; -import static org.assertj.core.api.Assertions.assertThat; - -class EdisonLinkAppenderTest { - - private Links.Builder builder; - private EdisonLinkAppender appender; - - @BeforeEach - void prepare() { - builder = linkingTo(); - appender = new EdisonLinkAppender(builder); - } - - @Test - void shouldAppendOneLink() { - appender.appendOne("self", "https://scm.hitchhiker.com"); - - Links links = builder.build(); - assertThat(links.getLinkBy("self").get().getHref()).isEqualTo("https://scm.hitchhiker.com"); - } - - @Test - void shouldAppendMultipleLinks() { - appender.arrayBuilder("items") - .append("one", "http://one") - .append("two", "http://two") - .build(); - - List items = builder.build().getLinksBy("items"); - assertThat(items).hasSize(2); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java index b25410210f..55058a1684 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java @@ -73,13 +73,13 @@ public class FileObjectToFileObjectDtoMapperTest { @Test public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(FileObject.class, (ctx, appender) -> { NamespaceAndName repository = ctx.oneRequireByType(NamespaceAndName.class); FileObject fo = ctx.oneRequireByType(FileObject.class); String rev = ctx.oneRequireByType(String.class); - appender.appendOne("hog", "http://" + repository.logString() + "/" + fo.getName() + "/" + rev); + appender.appendLink("hog", "http://" + repository.logString() + "/" + fo.getName() + "/" + rev); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java index b681dff21f..045124ad91 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java @@ -11,7 +11,6 @@ import org.mockito.InjectMocks; import sonia.scm.group.Group; import java.net.URI; -import java.net.URISyntaxException; import java.util.stream.IntStream; import static java.util.stream.Collectors.toList; @@ -91,10 +90,10 @@ public class GroupToGroupDtoMapperTest { @Test public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Group.class, (ctx, appender) -> { Group group = ctx.oneRequireByType(Group.class); - appender.appendOne("some", "http://" + group.getName()); + appender.appendLink("some", "http://" + group.getName()); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/HalEnricherAutoRegistrationTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/HalEnricherAutoRegistrationTest.java new file mode 100644 index 0000000000..314dcf11c2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/HalEnricherAutoRegistrationTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +class HalEnricherAutoRegistrationTest { + + @Test + void shouldRegisterAllAvailableLinkEnrichers() { + HalEnricher one = new One(); + HalEnricher two = new Two(); + HalEnricher three = new Three(); + HalEnricher four = new Four(); + Set enrichers = ImmutableSet.of(one, two, three, four); + + HalEnricherRegistry registry = new HalEnricherRegistry(); + + LinkEnricherAutoRegistration autoRegistration = new LinkEnricherAutoRegistration(registry, enrichers); + autoRegistration.contextInitialized(null); + + assertThat(registry.allByType(String.class)).containsOnly(one, two); + assertThat(registry.allByType(Integer.class)).containsOnly(three); + } + + @Enrich(String.class) + public static class One implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + + @Enrich(String.class) + public static class Two implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + + @Enrich(Integer.class) + public static class Three implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + + public static class Four implements HalEnricher { + + @Override + public void enrich(HalEnricherContext context, HalAppender appender) { + + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistrationTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistrationTest.java deleted file mode 100644 index a2b72abc49..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkEnricherAutoRegistrationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package sonia.scm.api.v2.resources; - -import com.google.common.collect.ImmutableSet; -import org.junit.jupiter.api.Test; - -import java.util.Set; - -import static org.assertj.core.api.Java6Assertions.assertThat; - -class LinkEnricherAutoRegistrationTest { - - @Test - void shouldRegisterAllAvailableLinkEnrichers() { - LinkEnricher one = new One(); - LinkEnricher two = new Two(); - LinkEnricher three = new Three(); - LinkEnricher four = new Four(); - Set enrichers = ImmutableSet.of(one, two, three, four); - - LinkEnricherRegistry registry = new LinkEnricherRegistry(); - - LinkEnricherAutoRegistration autoRegistration = new LinkEnricherAutoRegistration(registry, enrichers); - autoRegistration.contextInitialized(null); - - assertThat(registry.allByType(String.class)).containsOnly(one, two); - assertThat(registry.allByType(Integer.class)).containsOnly(three); - } - - @Enrich(String.class) - public static class One implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - - @Enrich(String.class) - public static class Two implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - - @Enrich(Integer.class) - public static class Three implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - - public static class Four implements LinkEnricher { - - @Override - public void enrich(LinkEnricherContext context, LinkAppender appender) { - - } - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java index 138387938b..8a00c69229 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -15,7 +15,6 @@ import org.mockito.quality.Strictness; import sonia.scm.group.GroupNames; import sonia.scm.user.User; import sonia.scm.user.UserManager; -import sonia.scm.user.UserPermissions; import sonia.scm.user.UserTestData; import java.net.URI; @@ -170,12 +169,12 @@ class MeDtoFactoryTest { void shouldAppendLinks() { prepareSubject(UserTestData.createTrillian()); - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); meDtoFactory.setRegistry(registry); registry.register(Me.class, (ctx, appender) -> { User user = ctx.oneRequireByType(User.class); - appender.appendOne("profile", "http://hitchhiker.com/users/" + user.getName()); + appender.appendLink("profile", "http://hitchhiker.com/users/" + user.getName()); }); MeDto dto = meDtoFactory.create(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NoBlankStringsValidatorTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NoBlankStringsValidatorTest.java new file mode 100644 index 0000000000..1929b0bb06 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NoBlankStringsValidatorTest.java @@ -0,0 +1,28 @@ +package sonia.scm.api.v2.resources; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static java.util.Collections.emptySet; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NoBlankStringsValidatorTest { + + @Test + void shouldAcceptNonEmptyElements() { + assertTrue(new NoBlankStringsValidator().isValid(Arrays.asList("not", "empty"), null)); + } + + @Test + void shouldFailForEmptyElements() { + assertFalse(new NoBlankStringsValidator().isValid(Arrays.asList("one", "", "three"), null)); + } + + @Test + void shouldAcceptEmptyList() { + assertTrue(new NoBlankStringsValidator().isValid(emptySet(), null)); + } +} 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..9df7273680 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; @@ -213,10 +211,10 @@ public class RepositoryToRepositoryDtoMapperTest { @Test public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Repository.class, (ctx, appender) -> { Repository repository = ctx.oneRequireByType(Repository.class); - appender.appendOne("id", "http://" + repository.getId()); + appender.appendLink("id", "http://" + repository.getId()); }); mapper.setRegistry(registry); @@ -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/api/v2/resources/TagToTagDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java index aa8eb3e7ab..cd3c18de27 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagToTagDtoMapperTest.java @@ -22,11 +22,11 @@ class TagToTagDtoMapperTest { @Test void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); + HalEnricherRegistry registry = new HalEnricherRegistry(); registry.register(Tag.class, (ctx, appender) -> { NamespaceAndName repository = ctx.oneRequireByType(NamespaceAndName.class); Tag tag = ctx.oneRequireByType(Tag.class); - appender.appendOne("yo", "http://" + repository.logString() + "/" + tag.getName()); + appender.appendLink("yo", "http://" + repository.logString() + "/" + tag.getName()); }); mapper.setRegistry(registry); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index 9924dae81b..ae1d75dddf 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -155,8 +155,8 @@ public class UserToUserDtoMapperTest { public void shouldAppendLink() { User trillian = UserTestData.createTrillian(); - LinkEnricherRegistry registry = new LinkEnricherRegistry(); - registry.register(User.class, (ctx, appender) -> appender.appendOne("sample", "http://" + ctx.oneByType(User.class).get().getName())); + HalEnricherRegistry registry = new HalEnricherRegistry(); + registry.register(User.class, (ctx, appender) -> appender.appendLink("sample", "http://" + ctx.oneByType(User.class).get().getName())); mapper.setRegistry(registry); UserDto userDto = mapper.map(trillian); 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..a367d171a1 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,16 +135,14 @@ public class GitLfsITCase { } } - @Test - public void testLfsAPIWithWritePermissions() throws IOException { - uploadAndDownloadAsUser(PermissionType.WRITE); - } - private void createUser(User user) { - UserDto dto = new UserToUserDtoMapperImpl(){ - @Override - protected void appendLinks(User user, UserDto target) {} - }.map(user); + UserDto dto = new UserDto(); + dto.setName(user.getName()); + dto.setMail(user.getMail()); + dto.setDisplayName(user.getDisplayName()); + dto.setType(user.getType()); + dto.setActive(user.isActive()); + dto.setAdmin(user.isAdmin()); dto.setPassword(user.getPassword()); createResource(adminClient, "users") .accept("*/*") @@ -175,8 +168,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 +189,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)); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java index a912f738e2..e028857e2c 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java @@ -2,8 +2,6 @@ package sonia.scm.web.i18n; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sdorra.shiro.ShiroRule; -import com.github.sdorra.shiro.SubjectAware; import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.commons.lang3.StringUtils; @@ -42,12 +40,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.Silent.class) -@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") public class I18nServletTest { - @Rule - public ShiroRule shiro = new ShiroRule(); - private static final String GIT_PLUGIN_JSON = json( "{", "'scm-git-plugin': {", @@ -88,15 +82,15 @@ public class I18nServletTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock - PluginLoader pluginLoader; + private PluginLoader pluginLoader; @Mock - CacheManager cacheManager; + private CacheManager cacheManager; @Mock - ClassLoader classLoader; + private ClassLoader classLoader; - I18nServlet servlet; + private I18nServlet servlet; @Mock private Cache cache; @@ -106,9 +100,9 @@ public class I18nServletTest { @SuppressWarnings("unchecked") public void init() throws IOException { resources = Collections.enumeration(Lists.newArrayList( - createFileFromString(SVN_PLUGIN_JSON).toURL(), - createFileFromString(GIT_PLUGIN_JSON).toURL(), - createFileFromString(HG_PLUGIN_JSON).toURL() + createFileFromString(SVN_PLUGIN_JSON).toURI().toURL(), + createFileFromString(GIT_PLUGIN_JSON).toURI().toURL(), + createFileFromString(HG_PLUGIN_JSON).toURI().toURL() )); when(pluginLoader.getUberClassLoader()).thenReturn(classLoader); when(cacheManager.getCache(I18nServlet.CACHE_NAME)).thenReturn(cache); @@ -194,6 +188,8 @@ public class I18nServletTest { assertJson(json); verify(cache).get(path); verify(cache).put(eq(path), any()); + + verifyHeaders(response); } @Test @@ -221,6 +217,8 @@ public class I18nServletTest { verify(cache, never()).put(eq(path), any()); verify(cache).get(path); assertJson(json); + + verifyHeaders(response); } @Test @@ -234,11 +232,16 @@ public class I18nServletTest { assertJson(jsonNodeOptional.orElse(null)); } + private void verifyHeaders(HttpServletResponse response) { + verify(response).setCharacterEncoding("UTF-8"); + verify(response).setContentType("application/json"); + } + public void assertJson(JsonNode actual) throws IOException { assertJson(actual.toString()); } - public void assertJson(String actual) throws IOException { + private void assertJson(String actual) throws IOException { assertThat(actual) .isNotEmpty() .contains(StringUtils.deleteWhitespace(GIT_PLUGIN_JSON.substring(1, GIT_PLUGIN_JSON.length() - 1))) @@ -246,7 +249,7 @@ public class I18nServletTest { .contains(StringUtils.deleteWhitespace(SVN_PLUGIN_JSON.substring(1, SVN_PLUGIN_JSON.length() - 1))); } - public File createFileFromString(String json) throws IOException { + private File createFileFromString(String json) throws IOException { File file = temporaryFolder.newFile(); Files.write(json.getBytes(Charsets.UTF_8), file); return file; diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-group.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-group.json new file mode 100644 index 0000000000..f665c29ee7 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-group.json @@ -0,0 +1,3 @@ +{ + "adminGroups": [""] +} diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-user.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-user.json new file mode 100644 index 0000000000..61efcb1609 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-empty-admin-user.json @@ -0,0 +1,3 @@ +{ + "adminUsers": [""] +}