From 20714aee0d2d2a989d93d6065e081aed8ac85fbf Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 10 Oct 2012 00:05:34 -0400 Subject: [PATCH] Finer-grained repository access permissions (issue 36) Implemented discrete repository access permissions to replace the really primitive course-grained permissions used to this point. This implementation allows for finer-grained access control, but still falls short of integrated, branch-based permissions sought by some. Access permissions follow the conventions established by Gitosis and Gitolite so they should feel immediately comfortable to experienced users. This permissions infrastructure is complete and works exactly as expected. Unfortunately, there is no ui in this commit to change permissions, that will be forthcoming. In the meantime, Gitblit hot-reloads users.conf so the permissions can be manipulated at runtime with a text editor. The following per-repository permissions are now supported: - V (view in web ui, RSS feeds, download zip) - R (clone) - RW (clone and push) - RWC (clone and push with ref creation) - RWD (clone and push with ref creation, deletion) - RW+ (clone and push with ref creation, deletion, rewind) And a users.conf entry looks something like this: [user "hannibal"] password = bossman repository = RWD:topsecret.git --- docs/01_features.mkd | 13 +- docs/01_setup.mkd | 68 +- docs/04_releases.mkd | 16 +- docs/permissions_matrix.ods | Bin 0 -> 18502 bytes docs/permissions_matrix.png | Bin 0 -> 42519 bytes src/com/gitblit/ConfigUserService.java | 148 +- src/com/gitblit/Constants.java | 72 + src/com/gitblit/DownloadZipFilter.java | 2 +- src/com/gitblit/FederationPullExecutor.java | 49 +- src/com/gitblit/FileUserService.java | 142 +- src/com/gitblit/GitBlit.java | 71 +- src/com/gitblit/GitFilter.java | 46 +- src/com/gitblit/GitServlet.java | 35 +- src/com/gitblit/GitblitUserService.java | 12 + src/com/gitblit/IUserService.java | 22 +- src/com/gitblit/PagesFilter.java | 2 +- src/com/gitblit/SyndicationFilter.java | 2 +- src/com/gitblit/models/RepositoryModel.java | 7 +- src/com/gitblit/models/TeamModel.java | 135 +- src/com/gitblit/models/UserModel.java | 177 +- src/com/gitblit/utils/JsonUtils.java | 20 + src/com/gitblit/wicket/pages/BasePage.java | 2 +- src/com/gitblit/wicket/pages/ForkPage.java | 2 +- src/com/gitblit/wicket/pages/ForksPage.java | 2 +- .../gitblit/wicket/pages/RepositoryPage.java | 4 +- src/com/gitblit/wicket/pages/RootPage.java | 2 +- tests/com/gitblit/tests/FederationTests.java | 2 +- tests/com/gitblit/tests/GitBlitSuite.java | 2 +- tests/com/gitblit/tests/GitBlitTest.java | 23 +- tests/com/gitblit/tests/GitServletTest.java | 217 ++ tests/com/gitblit/tests/JGitUtilsTest.java | 4 +- tests/com/gitblit/tests/PermissionsTest.java | 2391 +++++++++++++++++ tests/com/gitblit/tests/RpcTests.java | 4 +- tests/com/gitblit/tests/UserServiceTest.java | 84 +- 34 files changed, 3551 insertions(+), 227 deletions(-) create mode 100644 docs/permissions_matrix.ods create mode 100644 docs/permissions_matrix.png create mode 100644 tests/com/gitblit/tests/PermissionsTest.java diff --git a/docs/01_features.mkd b/docs/01_features.mkd index 2baacf20..e1c0afa4 100644 --- a/docs/01_features.mkd +++ b/docs/01_features.mkd @@ -1,12 +1,21 @@ ## Standard Features (GO/WAR) - JGit SmartHTTP servlet - Browser and git client authentication -- Four *per-repository* access control configurations with a Read-Only control flag +- Four *per-repository* access restriction configurations with a Read-Only control flag - ![anonymous](blank.png) *Anonymous View, Clone & Push* - ![push](lock_go_16x16.png) *Authenticated Push* - ![clone](lock_pull_16x16.png) *Authenticated Clone & Push* - ![view](shield_16x16.png) *Authenticated View, Clone & Push* - ![freeze](cold_16x16.png) Freeze repository (i.e. deny push, make read-only) +- Six *per-user/team* repository access permissions + - **V** (view in web ui, RSS feeds, download zip) + - **R** (clone) + - **RW** (clone and push) + - **RWC** (clone and push with ref creation) + - **RWD** (clone and push with ref creation, deletion) + - **RW+** (clone and push with ref creation, deletion, rewind) +- Optional feature to allow users to create personal repositories +- Optional feature to fork a repository to a personal repository - Ability to federate with one or more other Gitblit instances - RSS/JSON RPC interface - Java/Swing Gitblit Manager tool @@ -38,7 +47,7 @@ - Single text file for users configuration - Optional utility pages - ![docs](book_16x16.png) Docs page which enumerates all Markdown files within a repository - - ![tickets](bug_16x16.png) Ticgit ticket pages *(based on last MIT release bf57b032 2009-01-27)* + - ![tickets](bug_16x16.png) **readonly** Ticgit ticket pages *(based on last MIT release bf57b032 2009-01-27)* - Translations - English - Japanese diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd index fa1bcd90..1cebb3b7 100644 --- a/docs/01_setup.mkd +++ b/docs/01_setup.mkd @@ -244,6 +244,25 @@ All repositories created with Gitblit are *bare* and will automatically have *.g #### Repository Owner The *Repository Owner* has the special permission of being able to edit a repository through the web UI. The Repository Owner is not permitted to rename the repository, delete the repository, or reassign ownership to another user. +### Access Restrictions and Access Permissions +![permissions matrix](permissions_matrix.png "Permissions and Restrictions") + +#### Discrete Permissions (Gitblit v1.2.0+) + +Since v1.2.0, Gitblit supports more discrete permissions. While Gitblit does not offer a built-in solution for branch-based permissions like Gitolite, it does allow for the following repository access permissions: + +- **V** (view in web ui, RSS feeds, download zip) +- **R** (clone) +- **RW** (clone and push) +- **RWC** (clone and push with ref creation) +- **RWD** (clone and push with ref creation, deletion) +- **RW+** (clone and push with ref creation, deletion, rewind) + +#### No-So-Discrete Permissions (Gitblit <= v1.1.0) + +Prior to v1.2.0, Gitblit had two main access permission groupings: +What you were permitted to do as an anonymous user and then **RW+** for any permitted user. + ### Teams Since v0.8.0, Gitblit supports *teams* for the original `users.properties` user service and the current default user service `users.conf`. Teams have assigned users and assigned repositories. A user can be a member of multiple teams and a repository may belong to multiple teams. This allows the administrator to quickly add a user to a team without having to keep track of all the appropriate repositories. @@ -257,11 +276,12 @@ The `users.conf` file uses a Git-style configuration format: password = admin role = "#admin" role = "#notfederated" - repository = repo1.git - repository = repo2.git + repository = RW+:repo1.git + repository = RW+:repo2.git [user "hannibal"] password = bossman + repository = RWD:topsecret.git [user "faceman"] password = vanity @@ -277,7 +297,7 @@ The `users.conf` file uses a Git-style configuration format: user = faceman user = murdock user = babaracus - repository = topsecret.git + repository = RW:topsecret.git mailingList = list@ateam.org postReceiveScript = sendmail @@ -291,15 +311,49 @@ The format of `users.properties` loosely follows Jetty's convention for HashReal username=password,role1,role2,role3... @teamname=&mailinglist,!username1,!username2,!username3,repository1,repository2,repository3... -#### Usernames +### Usernames Usernames must be unique and are case-insensitive. Whitespace is illegal. -#### Passwords +### Passwords User passwords are CASE-SENSITIVE and may be *plain*, *md5*, or *combined-md5* formatted (see `gitblit.properties` -> *realm.passwordStorage*). -#### User Roles -There are two actual *roles* in Gitblit: *#admin*, which grants administrative powers to that user, and *#notfederated*, which prevents an account from being pulled by another Gitblit instance. Administrators automatically have access to all repositories. All other *roles* are repository names. If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction. This is how users are granted access to a restricted repository. +### User Roles +There are four actual *roles* in Gitblit: + +- *#admin*, which grants administrative powers to that user +- *#notfederated*, which prevents an account from being pulled by another Gitblit instance +- *#create*, which allows the user the power to create personal repositories +- *#fork*, which allows the user to create a personal fork of an existing Gitblit-hosted repository + +Administrators automatically have access to all repositories. All other *roles* are repository permissions. If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction. This is how users are granted access to a restricted repository. + +**NOTE:** +The following roles are equivalent: + +- myrepo.git +- RW+:myrepo.git + +This is to preserve backwards-compatibility with Gitblit <= 1.1.0 which granted rewind power to all access-permitted users. + +### Personal Repositories & Forks + +Personal Repositories and Forks are related but are controlled individually. + +#### Creating a Personal Repository +A user may be granted the power to create personal repositories by specifying the *#create* role through the web ui or through the RPC mechanism via the Gitblit Manager. Personal repositories are exactly like common/shared repositories except that the owner has a few additional administrative powers for that repository, like rename and delete. + +#### Creating a Fork +A user may also be granted the power to fork an existing repository hosted on your Gitblit server to their own personal clone by specifying the *#fork* role through the web ui or via the Gitblit Manager. + +Forks are mostly likely personal repositories or common/shared repositories except for two important differences: + +1. Forks inherit a view/clone access list from the origin repository. +i.e. if Team A has clone access to the origin repository, then by default Team A also has clone access to the fork. This is to facilitate collaboration. +2. Forks are always listed in the fork network, regardless of any access restriction set on the fork. +In other words, if you fork *RepoA.git* to *~me/RepoA.git* and then set the access restriction of *~me/RepoA.git* to *Authenticated View, Clone, & Push* your fork will still be listed in the fork network for *RepoA.git*. + +If you really must have an invisible fork, the clone it locally, create a new personal repository for your invisible fork, and push it back to that personal repository. ## Alternative Authentication and Authorization diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd index 31c503c0..bb9b7340 100644 --- a/docs/04_releases.mkd +++ b/docs/04_releases.mkd @@ -17,18 +17,26 @@ If you are updating from an earlier release AND you have indexed branches with t - Fixed connection leak in LDAPUserService (issue 139) - Fixed bug in commit page where changes to a submodule threw a null pointer exception (issue 132) - Fixed bug in the diff view for filenames that have non-ASCII characters (issue 128) -- Fix missing translations in Gitblit Manager (issue 145) #### additions -- Added simple project pages. A project is a subfolder off the *git.repositoriesFolder*. +- Implemented discrete repository permissions (issue 36) + - V (view in web ui, RSS feeds, download zip) + - R (clone) + - RW (clone and push) + - RWC (clone and push with ref creation) + - RWD (clone and push with ref creation, deletion) + - RW+ (clone and push with ref creation, deletion, rewind) +While not as sophisticated as Gitolite, this does give finer access controls. These permissions fit in cleanly with the existing users.conf and users.properties files. In Gitblit <= 1.1.0, all your existing user accounts have RW+ access. If you are upgrading to 1.2.0, the RW+ access is *preserved* and you will have to lower/adjust accordingly. +- Added DELETE, CREATE, and NON-FAST-FORWARD ref change logging - Added support for personal repositories. Personal repositories can be created by accounts with the *create* permission and are stored in *git.repositoriesFolder/~username*. Each user with personal repositories will have a user page, something like the GitHub profile page. Personal repositories have all the same features as common repositories, except personal repositories can be renamed by their owner. - Added support for server-side forking of a repository to a personal repository (issue 137) -In order to fork a repository, the user account must have the *fork* permission **and** the repository must *allow forks*. The clone inherits the access restrictions of its origin. i.e. if Team A has access to the origin repository, then by default Team A also has access to the fork. This is to facilitate collaboration. The fork owner may change access to the fork and add/remove users/teams, etc as required __however__ it should be noted that all personal forks will be enumerated in the fork network regardless of access view restrictions. If you really must have an invisible fork, the clone it locally, create a new repository for your invisible fork, and push it back. +In order to fork a repository, the user account must have the *fork* permission **and** the repository must *allow forks*. The clone inherits the access list of its origin. i.e. if Team A has clone access to the origin repository, then by default Team A also has clone access to the fork. This is to facilitate collaboration. The fork owner may change access to the fork and add/remove users/teams, etc as required however it should be noted that all personal forks will be enumerated in the fork network regardless of access view restrictions. If you really must have an invisible fork, the clone it locally, create a new repository for your invisible fork, and push it back to Gitblit. +- Added simple project pages. A project is a subfolder off the *git.repositoriesFolder*. - Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135) - Delete branch feature (issue 121, Github/ajermakovics) -- Added line links to blob view at the expense of zebra striping (issue 130) +- Added line links to blob view (issue 130) - Added RedmineUserService (github/mallowlabs) #### changes diff --git a/docs/permissions_matrix.ods b/docs/permissions_matrix.ods new file mode 100644 index 0000000000000000000000000000000000000000..6df0b4db9cfd0535f044259f8c9b42f34e3623dd GIT binary patch literal 18502 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 literal 0 HcmV?d00001 diff --git a/docs/permissions_matrix.png b/docs/permissions_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..d463ae110c8983678d2865cb018f7e132baa9580 GIT binary patch literal 42519 zcmbrm1z22Lwl%te;2MHE!993z2*KSQg1fsrBoN#kg1fr~f)m``-QD3Ya!%jdx4Z9q z|NCC$W7lRwQM=ZfYs@jmoGagCr9}|oaNqy{Kok@GEDrz>G5`R^4Fe9klHiCB4|)Q# zmlyd2lnvwUfxdy#lMwj~yuAL)Y|W1WU4gX`RkH^G_;;^gU_f#z7U&|hgP4>M^dcM_ z1UQ_gPxB?{61Ibos)L}lrKO>j10ZN;sOw;8K;&%dU_vA!CMB!li-ZXPM1a_50Y#Vj z!zEXK$pu2-^u!`AFPZ8nSN^?(kQ%iWDiv(hdv$}AdT#fCzVj{;4TEBfxvLu0=pSEC zLqa}17|q`#HC{ZBxW@PYD&Gma_j)!vUgWsp+DUbP7;YQ{1{e2??BETvWb~ z+?7>Q>gN51=-UcO9H4&iQLJ12&###M`miAQ`nzT#bJ~_6FNzeP$BgASCBM4orG=@4 zr>oO*20GjO#UB^0Ehm)zB|{Ci#P4fEy7nLY9|l)gM;O}Le)spAEQ*U}w1S zj5$}f~}yn?pU}wAGnn#sHLi z_Ax$QPNn?MWK&|78LU$0yVr?*`RL^ShJ`@BXEp1(?ma4CREpc3T|_aJ*lsE-M_;k2pn>&hD~QcM~9qiYH#UQC-N^ zEXn#KPk1L?>jsI?8GT0s<3`E}INm;QTB`ZyH1*#$R$DfUehQ0}nlNi4CYbaK5Bept zKsm}QMiwy19jAfiZl}{%dMQsJYL*zD!8#aXYukTbd;+ZgjuyM2pgVfIhIwXa%~;q7 z8W@<~*{Qti&RjkC6~09@ZBMn2Z2qTDxpKR_E??#~%FzkUmK~Q?^Mobf&f%V!TR$EC z0?WA)D{!*-jm&4Ke;Yq$Mo9Pk!+Lf<1YJw3jq;W-g1ggAU4QxLSF?1F!Hl>WDv7&Y zdwI_(j6ce~j4a^hJiCj^;qqS$TubshuSZ^*VAZy}4&A*gwk8EJEGh97o^1Bt+ z<-8mROf9`(g=^I)FZF5>on{+J-*B1t5dSnM1g*^rf1z(W5)2Yn?8F6VS*M6t_RWcM zWWex;lTC0q${h|cou*#wd;nW^Pj_}qXRUfye){~a+SC!CvnCs3Ha zwNO9KO!eI8>hEGY53PjG(ut=Hy}5yH(kWrq?GI^GmS-TjzE2-9L#eBHUP@a^ks*t{ zNv^Z4-a!%l*m!jts$Nzh#qJ@&T|l;2@7Z*<5 zct-Iwyfd&s>oAoI^r_L$R*Xu9lh?Swg$Y0wxZ7PZ5M6NH-alub23&^KFWv+fmJ_(D z@0zk-<9+YX!)kuq=8&ekl3u=ggVg9b$Iklqi-5irYo~6_jjak_X_9C)rPKesK8&G?NV~Ti9Ixia?Ly`$&pV)>xpGM9yF}4EBbg$K?S)A7V1Eekw+3$*&3M2c0eBxU=u18r}{v z{7SfMy6CNIJ^j@qSC%Q>V^wXtxI$M%kS{Kqg)(rJ6wio0<2J?%UO1P_<4KHz zhzia|iyGwHqBA zz(&WW1^_8_F}TbYKLv_Wv?g9z(yJ0FgX5KO3Hl?Mh!ODVNZqDrSug+?hm&W?6#dW* z#xE|H45w&IWecF3ih-%EbO!ZdRPhAGKXK>#{Zi2zCiuz$&#!{d;(3QBF%=BJcB=U_9DgDxwunWoLO=U`uWcZZ16$@V(n@%?I&_6H<_rs5=6 z__cer^#PL}N)THejX%E{yDAN_+eU)lhBI=gNvYugk+pclfhsZg`14o&ix|jA`0cIV z!_&CN!6?_6aMv=!8LzvL|%KJ)J z{b|u}Z``M~(M6bv4ACal()$C6+{BJ|)y; z_ouK)+DPq3PDI7RG+Q+aiOfGJ29QZ4D5BKud4GlW4E$a<224bhEszWbOtyUPj#J8t7;J8^ zSTLU{JRGo=koNZS3mccWc70)!1%QI+Pbsx&I`uQ>9UadHwf7mB14uUZlj3k~PWOeR zK5tms8aH3A+Q#GsIywDW=(Y#|#RSC|B9X0xoj7lmzXJ>(XN52=1X9V;t%6=DU0OUnoGPyJWFjP;`lR*+!77rsdns9S1x3V1t#r&lPM zm(F|jy_jFsEW}$QfX?!3&$QwXaE7h%sQ_c!9l>K;0O043Us_@G^y>|Rmx050}V!HBS72z8Yo642ro{HgJ3$hMTKzw4cIS`T;MpXZzsR?FVX@VZ`vj@W5?GWymr~%U&!famt>_N` zhAysS)-o>mYojr6{B~j?7JMqs=B3dzT+?N$d#x|N0ZD^QGggF#@j4Gx9P-_->c(kEcAqfB2Q+Bak0uk&PFKmxhk9Rr94v4kDMn*f?GX&TpdjD# zvJkVDFApup$-WzbTRMCfJ7?b^kBe?LR_keOKUETJ_QAz;=NgKSJS^}Eqk|o-jNf|Z z>S^jJSI~Q{mb&aIYnw3c+}rIps#cE__UUtQenVYFn=N_uTCiKPUqjVZXCXu^ZbsVj z-ivW<|1SNhkVq#9!Y9$q$4yte{_}@6duKic$2eF+8!zI!~|#3F_ICZHF|heDbzC-jG?4*tzpJ1tHYB3aAJ z?wSyZVLI=2am!4+R)SI~+Z#7%Vf~0o(s+=&&@VnS2n#%=9JG(v<4L2u(PcKhTYH0~ zWAK37{?Q`|T>vG1@ST~BwOLBToB(|n1+8#nIl@<(66po5a3Uy%7A0~35OBSG5AR1r zo6xNc2l(i?*is9i(09zeJo5@;=%iRmqg*#@PqH(ZN!~aU!RSrLy_df5>O_u~_;WdJa+0kPN44o%t5{>4}}O#D5uG;2o2QNVGE3uxBc$L%S&;6t>j@Mfa5dpsjgdMCmUJACR993Jy_q8CeXNyuYBxAG6(IU#j;2VV z6H=PfM>Q@%sO_f6YqR^pbwaM>wAwjz&RU`!jTY5^{l-Ins^g{v-}2^m>+JpgOGrtv z0oMHYy2=!uIoYOVA_}Mev=Yp2O>?_=tEQ(DYN%49~Zo2Nko z-@a)&f&lXXlY(_p`dJx^cd2ejfyL1VKVK38C^%X9U?tF%B9bayw^UFcEuW5Ii>L&j zk)!BW<@;$($D4<0`$~RT!fgt`mmFeAzNROjpnCs^t^asQJn-_#MzQf3>8UH7v}>gkA^rt!Nqaq3j<#Vxkkcaljj1*!mq7Xx3nHM zG?DrcmhftZHN)Y6O|%8A<4^{XW4Ow998!ufq2eiCZei_ zFB%@m<*iTt+XS?1I*t6t1092f=&0(`67^+RSv^42V}D_q`v^tL`XqBLEw^&H!y$b- z<~rlvm&!Tr;N%Sf$hu$|y2*#OXW9bECN!Oe4v#z3!lmL)1|CX$JrWs6pP-V8m6-<- zgK1Gm?BmINS1%^*VX6o*=n!bSlriGV(J)&1CA+bnZ;Kna*DFILQ$h3(o=;)G+BeshZQzj*t^^)*7G^@8lO@yn3 z2-^~ZyaPQN%QJ+v#Y(_s*=1u+1WX_{$@5gsipL4J{q9jx+Uz1Ph+3mnqMl1&3YLC1 zf1QetN5jO#Oqh=PS-ke=EOx5?dQS;%&?MCqnzHF#+Aqk}?cU{Kh;Lx}VRMr@KggB3 z=f&-vZz26AO(nxdpk%SktY^7Jh2!uWEMCAeu*pkb4l--aqNx|W*D29`THM9R9-V5P zW8$csnE_69soT?yFlx&W%i0i`q-$!>?|7g#NsXoiw1h2!G9gmZ2{Y8i*tW@9i=Ztp z2&(j0eC8Xn87+FJ%<*b*mxb@|_=-?ZL(9rxB&Dhqf)|HuLT;-IOon zLWeWKCYSjxG_CK)E7Nf7txr@d&cw>q2Gck%&aI9RCCi=;DQ9Y)F;}*XBaWWL8zAeQ z=+DB7z9iFX=+RR|vlm64{7{x$yhF{*-qT#+c=_;j>dh~EaoAU96Oy)q0I_HCJ7C_w zME@c!g0=mN6L#BgbHBVdaa{b^OiY9Zan9ep=^}AV#;s+^NgnT>>J1Nrt-)%buD)Ps zC$sH+xmY`zx7#rXGV9mZiJPZ+y2Yc|up~r#KMSq&pN$%HE&Ybt9O23dV=Om6zZ1^|?ErME**&_Oa{!~POtnuQ(yoXRTW z_bCqC-bZ#8Ykg+}aa3arV2Ve^oT!}N!Afer7Usk8-K$bG7jDohhK7u2&OapcSnS^t zs~ak1?KtHa?Dd!gr6*s~6)T{3j{OP-8W3Pt8ARKl&izA>EMM;Jqh>10Ye zsor)pbFk@q<`~?ic(yqt{S*bapH8@B*BF1AyohclesBkXYg;;6>O+e$w)8ib%2#M{ zjsy&iATD5^M&`47<)ziVf#hH6lI0|2x*B5+tidTNQ`-q|%1b`|z)CE}R*DpRe9jZ4pYH<#hAd1gf5yPYDmD8%v8O3zPV7+6&s)&q1t#} z(r&3rwF4sM^r&`I3VxmCo0jJi+M-DfNh)n1r;55HX+J`#3QA34MZN)u7R2I__}|7v z|A08X1NV{=LkA2X8&siKtSKn+tEhg(&qTdM{r;-saWc+~N;E&1>k1QK&&s*t<+rMZ z4v5kWj`HRd60*}vs0BgAB8{U0MA`2JA&LUNzX4!wb4GQWN7@J3XY{#nrv@7vjMlO! zV%zMIq04HNG2eBg*jp2<)ZgrSjuKjIe9+ITX0wSQi5?J@3yD%mjg^IAtxQQhvN&42 zHUdsjXCiJsp2fo#l7sfoC@u9>>yO3B1C z#S!3}-9jM!I`LCc>-+nxX?G$46bQt)ZhrY+E+_&hMo=LyJ!KSQmNjwU1}otq-+u>$ z3yK_wJ&Uu>u_Zqwz(i9|=vUAKJKK%HxhKWxw4oQaHR%=idTc6+=FzlaeE8Z3nLNJ> zu4F{NT`o(-?U|0DXI>hKT3m2gRGpjd#^C|1_V2ckik{r7SBFZ!YBWmvgC!5#KcP!( zl#8?#U>=A$Jbz%ho-^9~t z)LG0`>2`da`1k0?dh<E+Wcj9%3#M$LBtN(=y!9F~1(n&KLYVQ0QO~Fl|UH>taxf znO)>#5N(?86OvBLJfAkJcg4NNgPUzOZF?xf#iK7aKJh-{JSHsdU%!PHpooAzROjop zJSr6*wYkQ-I7!=iJGPxyJWp@Chrnu!t7Tle;7R%ABrmW`qPz_P-CZVs7y)GlXI8Le zTW(fjS9n+SgW?1B2m5S2asHVW^UxW7xmjV7Fsw*-X6oF6mQ~x+o-OTQ)Z*uI+vJu7 z6=wcLmq)C@Jdruoa2#uDh{m-jwdi{cP5=_~Cga<;2<|G1mNx+4bZR{#e{n-fhT0Dj|7GW#b;V_73jUuWSjnx2cZ`4A8f7D}b3@ zog3EU0)?0QbfVo&m+a0SIA`ADn_Y|tO^MN?4THq#d=VB~*}CIKfX^j@58`IrUv3sNyf}i*zBG!yVo{}JI(v^95 zYh;4y1>Gh1btkWA11q&dp^I605mXQC%LY0dxqwsLJ3g+9pRRU{uQJTKgXs0y-(pZ; zUFhnMT(iVBa{q^J1Bth%WJ&xL-13%Gg{ zf^ya{<BjtaI zq#6&@Dqh206VPjJwWThi=et;y-{@|nDJJ1>enRP*3=Qm0f%Po5dZb#8@cZ$ObP_=3I`6v<_l@AV(^L;zA=P=qlJHQ%t|$_R}uXC zTDDmvYVZG*6QXPBoVe~H^}6)83G$Mxg}@;C*#Ik<5)g^Ls)s%LSeTkRkk?FOz*UJ2@W&{}Ls1q} zms1GbpAfR6QA6_cpYwgcg%u?r^byFHuh@S32}ZXrcvOb?HPL+az_KTyM`jUrjx*}J z+Rp~EY~^eNf7UQ5VP;QJRRMM&Fnr8%V+bgf7 zF2M}~44}2VQ5rfk1UHODFNbvLyEtQ&!f!+_4>m2=suOh*kY^0Hg!#j)Jk^W)2i~r* zV6&K)CX3YE;@ItIU2g)_zn%C;dNOaz@TQXai>Q4ME_>)#QlAiISiIYrEaiM0e} zA`bW%U@}{Nm4Mup+!d1pOvLM!xt1+g96`(Z;HT_peHyQ0B2#Hj6f*zG(c;6=*p=_x z)wq_3dl_Ed=^j}Ti=jq_XA8u0zO`@$GFWCcSBkVl3cFI!nHnCTzqftW!w5?0drx}1~J^R zy9qQ-lSZpNjenLuXt}(aH(5IGc&&acy@h%6W0{I-mJT#+S;~UYz^N?jY~1p) z)D+deu3K6a#u*B`R7g$uSClg%5=)CyN%~jANLC{V6xe^A$b1oaACiK<$;d%;kWI!? zSJz*F9m)em#oRX#8~T70)bkjpLjpv~j^ci_8vY?BT&3t!-f~sj36?ogv-(bf#oK@$ z5riDr;mz7t>duz!=(r4=pZ|)8m2g8Q+m<#K^uGJ`&95{!e#+=#!FV`lJCGG)m&V^z0i zX50zHqY2u~G9Z|HoJ(KMk+gesQd^AtK4YnA)4ROGZ!C1Ku?lgU> z*U?aN`Umu|?4>Ot3bJ=;dLA?J_kCiY*i8V6u9@*+@p}Me^{z>Uz*+Ub-^-n9mOjn6 zb9*P~2*IUq0+(+>TEh<cq6AY3EAB z5hGd?qgj~}7{9Vls3HFaCxY}#k8HZHILu!=zRkE#21z{x=1WvMo~g`Y2suT?W5 z;`Gvg>Yfi_1?CgbLciAJQRXfK3zsV+VeL5~{2!62sv#gU5OrO@6@yQwnt}X_rMgNp z)$q~_oah$&IpoJ1mcJvD9074=Rdpr;L)HMwC9vDA)tl9&XS;(rz~lS-vgvUR9hygo zqLMSTl~t+?bZU#NfN=j2>|kUDe67kM5afI#P31uR13n-)drh5}=AGu>=ka~{C#}gc zWh*q^gRYhaApxrwl9RtQ&0YFj^1k+dJh1g4Qj?f`NA*;n82!UqrkR}w?JDSWUk1!N zs$AR}u#SotUwD`)5RJ07KGDgdZf#8|QkeX8$!SQpPHdHoW{UrocwInves(oU^Yv}B z>Ja{Vf6z(BPruJ->|A~NcKp*6gCfxoRtE zI}N5@l2m#o!<_2c^|EVKi8TMTvnuQZofX5#1k$` zfUYnePjYkSzAAV3=@Je6NKG0*Tly=!fa-4^FN>qiwZBNoh$WhO>N@ZsMsGEN;YNtC zZ1yY24`c?mZG)>_?C*!WZj{1+jP{<9C8sSVM70--P<#Q&Rz*>8H5}HJR3ey!RSN=s zsZMH|PCYqAyRS3?s^xdg7CVw$pciR+c3w1OSj(vP13*}#oR@vC8tNN??>0Kl*FvnY z00_@g@@G-rFm`5lGx&hU8N>qS$*r_!^RCu%?btv@Td_CW99>{fCd6q>`F3&{uZIn` zfG2+26njzPqgg`25ua@=9RG4_&X4F>I(R2dDFv)%w*`<7U^vm~rPILErX4z}&o8Z~ zj6Z4EWdy@nv3GY?7-yibJznfK%639b19GauxNv?sX}0#rHLqFbWV~b-$sB8uNd(4e z3Y^<9l&?5YsabpJX9-Z179aW8OPimihkM+YF!OEry8Ed&z=3w|a&RB|U&p7l zR#Ndj8AuFJqXg4B3Sm-Ul!MfuwRAdQKLCb>AD4*w|Kt*2Skv#)uzwrPQ3{Bc96^}( zxbzC&|NiqdaiN2mHRW1BjDs9d#85tekrw(xcl`C;ML*lOme^}^5ls8bdtVRIP-wTx zROB>A;N{+RH#P3u1UcZ8C)=JIL5_w6ImMvKYui2sW||q1>IDy&wno} zr#hABWIDj-N3(JG(o;pOp)q&oQK(}x2UZJj6a9x~?WO~hk+@%I0foa_Nq?b zM%*StTp6}ovZPW|)EK}nZ#MplaTdbA(EP#UQ8z=bwi1UqV!>TY}Rf00Gw_Ib=uFVr#QgU_uB^8b}8d}fYn!_^`nKEFga z=s-^Ae9c*E?{ai+@5(K3S<*h}`U?8Fx;cb){S5VK)89#F<(CEWKeEvCyO!~s>b47t zc?5Eh9Idd|s-xh}4tnDyu#4tLw7pdJ^P&|5%A4g@oF^Wru{$%6_IIze0+gi%TRmcndC8nfeTWR)ri6Z-%x(FJU>`AvwVmo5o zFq-4Fj6wN-S!WsUVzdIKGE2HQN<{xI81%WVNEB8R`Pb+0>oK?7N)3#| zhUB~tPFHo7#lnMmflqBV?jDe0SF=8u*(Z?MY2`Q{~%H)S7CKQ zZ}uv)tE*d^#?^sH<@o3QTV}Oq%Kw0Pw<=c;Z=@|Jx!Gx$%~cZ8fh!K|+{LQindpX0 zj^&|&%;3h*X5w7#Z8J0cY4+E23_r5@{_PWfr(z_OPe=+}W87nwOG5 z65vxh3?km6YDG*yu3>kr0jL`@@`zLdI`gZT*cjNY2UvSv4jfPgu1O1#JlNM?NcteY zj!gLXJu_GHBoX*K65mY?*&{E_ytPvVhz8d^xY@W*7>y4~kc-E1lxETT=s`H++k*99 zKr;lpZaiT(nuDeWaO{^gdx}u9l?uL%iR)E-4IYq{@^n~M$YvKc5qGy^Rq8~8!`4y{ zmwUSxR3<12FS@+8Dw38%o;ZlD{Tke0Dxc%{Ip8ZawyD0qu^k9iKtze@_w}DUnP>6gaa1D&`ANwW>5~44y3g^nEkr1P&E7YRzv|>@R(FF7 zn8!hG47r8rsWV#O2cT|SlzU|ta?EdRLJH6-?CdwD@a)HlT%Er-j^Hx(SD8fW)T<3D+ywE zhunw3mAs5v39J)#-pJoRR!N{OZYD0>g#^S&cK3seTV^rNCPhzX^oFDWJ}f&{6lqEFC1CogbH14o^#l2;YBSRc@EL1RCj-j#X{-%x4Z zj$T=?$J2rjimlfr^^M4hT_Kcsphm{svX{O{ky0Fv(=#~h>in`)4**aiQ#G^|S_sXh zG`I#Oqe>^`P<>*fy6D3}+#>B)Gs`H$thwP3CIemx)QF6mCASXOVk z09j|B&tos2rW!Azmy@miOY-s8@a_SSKR1~UVl%H{_`OrIu zkj@kFBCp0|ND8HUNb=o>YZTv^A2AFtz@%;0P)vLgOl&8EFIWFt&0zMO->&2{yi~v0 zye!2-jLSUT3roq;Ajnrvo88Fixb0vXTt_`XFayw9j^z1gJAYs9WBFc9pYpy_ZP zF1wLG#97S==R*2B)?RF?86IB#V>?4tiK@3F`YZu2tIl2bzxEdl=df)H z-&u2D#cOs#QXLU1DAN-D`nbn*MSMMSYezOd(B~_g`%dpkqAmptX0ufEj z$lVd#H!mgW5Gw5O-?DTa+VX6;_mvC zC(*B`?{RH?BkPTObmI6}8vU!um4_05bJBi3puLUK@e=QAOAdiDd1 z6am`oo5F6!o6*YCP|H@coKfumRML-3UEr0tS)v|IX|p}pXJn$nWP5Vr&aO1oD9oo< zNosIKP%iMs-K#&5V|9W6#c(OmdmZ`J$lWpbPl#jvZkM#h%r14DMpex0T8Qkxr;7Ja@aYhTRkrJoj0hmXjYk&WfjolD*FJuCTku zlZ(V( z@m^F}1d5u*GI&3JVdWIh@3=xLgjk3(qnB{=EII7q8GZU4k2!~pS5}A?ehgZHAnH{s zrTAa<->_?9=*?(F;R~OdmT+!-;P$@_?G|lmX(661tJO02Z))g^+auAZW20%OGD^{) z8^t|Q#%a|%A50VAPZS&pNP@KqH8s^!xw=1jiy} z-~5HQ`_xPTC8|YIUR)(E(OM`bZdoIO6_$juRf> z{oN=KBX0n8IU&Nj%<@d3>Rd;Ao+x`_ zW>PW<3ivW#%fxy7<$kDZoj5iE+{6XDrg6(su5Gpq;g8x8NFNxAfDffJxF(oZ*d6!_ zvkDNMrD~%>Q_FtyIh)sNj8&|)Dp7XJSbru<7;CyYETz4^-DrulWEuY&745)G-Q6e6 z$bNV_=djg(&^yHjYW8e8PZdd!(V=+*NUOW)5*A@0X08f2cvy#8)#-Q)fLP4rj;&4l zXU)8d0)9IhrtA>|u!<{(xtfUlL{ytVtK#a>ERGP1l`+q5d4e=P7v&j&y>7;p$Q?Ur zFp|s44p54J^$&1EV%J*y!Ei*7f|tXQq}CvWiWK;Q=j@GUO+wKdOdf}wI;=Z+P+cUVgfBC1_gF!eo(8HugJA5o&3&X zJ^Ly90qdYvFE9PGDI6P!sAYK{@KJ*lPUz~-6_Y)`|W$00BdB1_QjdbL)59kRl zmEtwSfr@T7|Jb`T=#}ISoEb=Q?Z=|Eu%fSZfWc--j1Bo9lt2e~Q7-&C04+pA_u0kw z<0v4E`NUVTvFhkvicbDdN+yd9>A!uf`ryBOtn-y>c~Qz;9AxaBW2*%%;ad=e7TEo7 zL#vbFk;GiSJk2}HkZ(Ft=&7v~LN=EZJ0kU%V?ti4q_%yJ{WB<`{uzAg__CEcob^ZA7ELQ_WKXJmEaX!0-T1SP0nAf+HEZ=4M8=FmLtll+aK+QLVcYDTS`+kU z01C%N^Vg3o4zm7HEkhs+MG#^*tX4Aj8f%gc7H-y80j_|blA-FF?hGws#{Szxao)yH=qR)TD1W2Q$F zakto=Q^^ESx?pY`jdc`behzYMiK9%LKO-z~OoS7`(5Pgba?^vFH55AjRM5w%iJ7}hk3W^7=Gd_w#5}jzv>povyr3W9|d`~#!+GIvOzGZmX z5F6^PH#`q*QzaI%TKu1=&9KH}<%k{pYb_Qnr+e9D3_uNnsO_S1V8r3K>@57OWIQq# zj}QN!JgNWxV9v$?3d?EVot#kyfAZ{3aLR(MO`evxF36!XD`lwUOX=gFEL^>^Jo>~Z zU7@POv)U1rhHIlUJ30*fOn>ou0`*wjg*IBWWL?SMyDQpQfu%P zS`*VN(VQbNLk`(sc%#IO%K_>JYI^#C%96^{YeK0Q@t=yD8^{@1Rbc@o70c1&!Vr`$ zDEKQ2KT=8~(GPQYS2%aXx(wk$;YjP>Gv7;M02(q0QV%ZcVltf~6wSkM9xz0FBBq zn{mQ;1ZOEt3@oqoCfs?!Ic=_Plk)@GrDeVhex4-5!-F6TnJ&#&#@>GgW1Y3Z(u8H$ zMb912B>8>M!ySgyr4+S0+d7IYcGu(6O)a@z?gSI$jH6nyY3HqO=9CB%4wxr4MaY9I zu8{hNG>#f_0xUQv{Z{4QlQEQ19ZsNie0kF0Y8SQZD4t58Vt@m_(=W6O?Su!inQA+` zT@-P5<>E};=?`(Qw6`zM#8iP331j5`%J?cR zkW(~1EY2TT@N~^WDf7e)R4R2d`NPmDD)d^zwk`n!TrrEGr z^(%XcwoWqm;&o6shfHPiI)XlUOH9{mV)QYzeEelQQ>=YZc2uvYpdKDVJ=O2B=`fNM zZ>CqO1+jw9{8t>@UsXtwhtDMBfvf4H;@NI#j~2b(iv*0rPKIs@svt0};>&ffqAlWT z#D?{fQ{IM(v&C|^qI&Ecx-oQ@fl2Q-u!H6dRp_52E_*!{bAqL|F$v=7wwx77!ZTaeJ*cVw&0yPm_~@=J6pjC@nYV%Em-r<|GXvK+AMJK@ccM(5*Fw(TPk|Mua`hR_f!E4{J8M zWParLQfEv^c()EhYMS9Uzw3$du5(yfKwN(a-&pLmrr^0WhdH&W!X~9mo^h7WX$I=g z7wjEb!H=65_1FPZ!vGQUe*$hx;a7p|1$rDG4dpUlwhSLy~h$_=BbfrC# zQuo5|tEt$#lz_d;T<3ION5StQ(wgEOp-GfCnAlDCi_6bXp-=lI`E~C6J(vG4);PT{ z?XTOLK`2M_mz8i~|H}!Advh-=!!)0+V}!a8+@}P<9Q4_qvO6~^Bj2}qG@Uo!6TuTw zU{;vAg`hyy&ckd6o0fgY3gr=X)bA9X3nAN;ld}tWwE2{Hsr++=n9k5$JX?j9fKmkg z^gvu^ncK}H*j=F8_C*&|+ASwpsZn9wUP4@SBxWrBakoh$=Er}Vcc%oo1rF5VghBxb zLV};(0=JL=kXcaN2V{y?7$z(C@cRChw&E+AlhhI9^3E>J*3+H{9!{FAw|Qj6qaY)* zkDC^p1KuNo3>m$uffDvswcc!NA(m|>L?{#L>Ia(mv^oiwozM7bYpgB3sM4vM&PC(-ha32DSgxEYfSF2 z9pKX7Vds8koRTL-HNax6{%;~+^rqwBdbIqk`tv_EDE2bpn0EOrrpQqVZ9zu(v(w*j z#;QWDt3$Ydo#KXbhh2ifhYd30gD|pEl!V<)b~f~q==`jEps@>}X)bz`lk=YUrN0&{ zP&ngzTv2WB#&abS=u^3l>&YnLeM<8O^E_LEd^8qd`0Wk`>iw2&`!BvKHL1BbJhpj4p;x7V=geAldkn$!@|hO zi1p3#&jV}2huQMk+6wlI7*^6t@)7(pswmsv_Wf-vB;`^YoN^bSVn?((g9o2tg?76> zCl6wt{6BgD#u)ELlMQi4(47enw%8S9z;W?G-jX?}AMbzUo`TZjrH?XJ#h~kdOw3Vl zPhZ39nmu zuteF7g%tD3%mzK6#)`^wvY z{52gk=vf(@e8~q3Q$gj~lY=J`Ci9#o1n2#UTQ91A+U>y^D4Fio1Op&7t1fH1(2vS( zO}s~w!q;5E)L3cPUG)3P9$|-@Hf5gvZyh)5uEYxD_`jzzw&|TW93-9a72Lai3=Ikw zgo3GEP+Pnl$H;l3Px%K#KYy4ZyAau9Dg;@&qiI}%$1T(w{tb54%}F%d>V-8}v$_lW zrws7K_MbrY8d|oL9a(#B%JP+ufz6l-Q+beSi9fo1dJ-Neq`0&gZ3bO+`~uk-NI;Hq zkN|WEIO#afU?|*q3VN)fE#$v%nwZAS&~va;WiN+@PG>AbUW%`yRmcJow@w55WS3?~~!dI^%G)=06Cte{fpbCf7}>hSO}= z5ngNxr>|nYP-j+Sp+0*8U*D?|xj2*0e!D`w*J`rk`_y*4U(Rr{ggHp80b>Dk)i(Nx z@}D$HWBB#E`%nRC2amVNJ{plW`)njP+>ZN9=jEkcIL0y*4EbO4`6ZJbZ#lCE@^;X4Er(2kp-*Gy0 zlD6ARkX_3$?g!W@d;nCQP}uc5^*$ev*2^@gUuVCeM?g`GAncNC@Vw~YqCj`Si$P?X zzHZI=F!%c3X@C+B{9lQuozOpzsdjRgU%9;)956M+#sNm>%xHZ&38~{VaO;>?iDj%G zCAlnNy_ElJ4?uZ*j6y#&@Vdi*6Obj_N(54i&gbEz|E+d)=}T{(xAjbbYHMKL^%e*U znv!3MBMHz!?klH8(T16eJ?cN`n*akS<)Jo$Dy)N-Tom<&8`8k4NJMVF=*|3{BHzs} zNzw&*xGfP-QT4v@8R!(}rIdBeRm^=@=HfJ4jQ^USl6}%1u7bIe^ecmj zVEp=!pT!|e=TEI(YRyGhHINsFV?Kl}Vo04V$-6i|bQ*eN-ke#%1E^U!PL25{du)B0 z0+$9VeevGBq6XI^R!2Nb()hfon(yLDj~jR$W;5=N5h_btTSBZS^EHy1GdAJ*P7Dy}bE_bw#3 zyGw$*y9Rf63m!bUOCWe~3-0dj9^4DJf*`@&{jGGLK7G2+zwa1#+?P=w_6KT@U2Cts zp85RdTyr`4qDW&#|FZW z`Y!Uz42a}x=e@L=)blTuwUbcXrgdyu=OLbkpE(lXj2Xc@Lmsq(Qnf6XFUw^fxoPvU zn>zXmK97*1XwX(f9>p+LUWzjPS|gm;EW+U7TMV=SoSu+to00vnMu)bFSZjB$4wqYM zb>2JMY8C9D{|5daGB{vHuQe+81fpmKyg}|xaa0le>k!@Ot7-RL2k)LFcbYc4f<-mdyB5Su2((JMDrylph|7wvTrUESrB854~N|}vw*Nb|0dg|RPLe}L``q-m1w}JGG%!D zT%d=VY3H{j#l?Ra_sJ@tIhtNuE=4YGEmp3$jV+3@cSj|?i#XdsXiIKa>Wp<_HrTiO z>F@Ey;C^OR#uGByinROPRmVylJIW*SM!XHXgLR~uo)Xn1mICagKJQ{KCE~Y(l=qsp zQHmbSkl6KI`FoQWb*dFSt&0^GvQkY3ij?0U74-Pz8{B1fqLA{;-*jw(GQMjA#B=9p zu<&!yF(ji%x;Q)6wKs55P!0UG23ehdH2EY4#X+Jy7wiv5JuwT8paetHCt> zo(|u#lv=2aY)e)rgJ|-D-Gg_gMrNUD!Nmb8%d3brh)GU4nX&Os^BUr*pbr66?sKj}CE~zktPu3*;bN zf-z{r1@Su5e9*{V9BFJ1hA=RoxT;A4qSojRB105deP9?L866)Vmsj$0xtQx707x!` ze9v(9hqiA|zB~Fpv6{@=+Ze{1PICEm!~~G|TL07*vKUClTz}b&yC}6rPc?8r{RYPy zm$TW%;MABsv$_-};hpfUAtJ%yC7#8V%{ zM?#au^?f&tSK1ika=Ro)sQbBxcF{Gq>%I%(C^h9l?DH^3Xs@OCMb=>wsC|V{_Ildv z4gq~l=zu{zDFay5UhtX#R2ELsd3z2{7gUht1ffOKFgYYR-9#hyn~ggUO5a5s674>s zB>8`ng}Vb!EQP}?T97v#Di4lZJr1&+1_tJ%2sBkzK8$ucOO)>_sgBc&%hp^t05I(| zy`usppPKnl9L?aHcFSk9rZ8mDTZO_ArCHP_f8|HzZCKDT%mBp+0I;}`Y=eeasPC#V ze~l0{nSxO+4i@Qy_r#dH6{UD6@X4}3D)rUIa#txO7M!2p`suJaB`J?c+Aioglr_{v zcdFt=5seBX^mUt)}qNLACh55SJhmJx3zofFC^$ zX6g9SDdLsHhePi${XY&S4x4}C*%r*#6zhG{Z@Bo)x9HM}Ztv*#lLkRdB3$WQ{KJ4}89nMFj)^slvZ%c_*eYiboL&;t7TyQl5uJWO<8;7F3hBMwlf$0?m&x4aDsNc?QIXp z8`<=t$B<7>ONR}KmRHx;;7#qGIRX>Vfy-P!M)0f&Ymkqg&mq}&o&QTkYn%CG)hifQ z{^&KCjv%ws%aQD9cqJ2e%R$m%{Yw`bhi&Hy{homV%kUuir;QViuYn<1$!r1fX-<_l z_))n%fZ;Yw!ncN{(V#L+Y%9^_--?QoKeta-PZ4fTGRA9{z!&{TWYS=c7eKQobHDmW zI8{atqkC-zZ&h2mtOpBvYW#J&QSeR6nN8Ev)-8Tg@M8`vdP7>;`psCWgtfB@URCe% zW4=R18J)EUZ`*2fdXpKWZZ6lzpw(?dX466wT*j{3O)kLIpjJoC2hS-N zPFPWRyk%4l4Uwsj;L*OFJ`XC8PH>jhy>c1*&l=v9@=p!VUKNWag}Vrqa%?6wTyoPY z7$@GI#@HqmMca`A)C&eaizHGL>o?IEmRN97c;IMZlJ1D0K|hf9@N-s?9s59#s#(e6O93H*xNKiUl~3ZBTZZ$vZg zP#;?7k-m_!GYjz5hQ+#eQ5+3_W=i4b4WYiGV(C>9XgpgoC7IcuY1OyS7VWxfdP`t@ zjG$Z8%tZ&j2cHn=`Mi`S$P9f+%;kRGDp+8Z4>J8Z?Kax?yu*)M{n~kGu{L(YZ{ecy zR-l2Paz2%e%zrg_?P$g90DIVtd}UiK3;QuP{mZYp38*V)_53F9W)t7S#uSUGfZY|J z8>2+4Yzjv3l|ur^q+7piq3cy6iJzTj`0ATovtN0}=T(R~TNZO={LZof#}a8=ZR;?o z?u(Xy`v)XAV$QR^5!X-yRC$(dVSyH==lzoc0f?c7n2kVBLpvLPi0?EN&VlsK8^f%j zPJmBC!+xv~{%3fxzAcKVSTj|t>-|Fp+2DY9U-wk#=HqCmOKx-A_5Shql9~SIqGrOQ z)ve3iqq9*S3zXU9DJK?HKwvf&ajWB7AvG_g@|=z35xePV&H@pi%JcDXo2$xVJOV|} z!r7*P@o51B%scM>z&?;RbGV@c5ocaWR_;)HcMsQXSt64yPGa0pW` zTCm2Uffn&w_508m!JkL1Tc2sPi2ZH9k`1H2Tg{QX>*KdXF)uj0;%jwlX2~g!G$UzJ zeT2}QYq1LOVXi7$c3mQa3izImTE{;1g5GkX3#w-4a?tw?z4-pOt=Dk1=bL8y2iLw? zoYP2Zy%~T@2Cf9d>4#Qre^gR*oZn{V78Vvo)WU5ZhZ`n<4>n?wCL~nXh-H zLS>xV6VKPoiGiPoprVpNCTa8+t_p6!MiG_FeEp5w%NS+&Um%jp^&Vw*W!Pc5iLn;% zo3C*40#e8|T*d@XO~Q3Mj|V?XG0PPx5Kgm8z57xV51i%yp?wWwC)Ab)_|aF%+HJ79_lY0LGKXx zN(H;O{0|2&s3|jDgE0c)=k~%&+YBB`7UkjSc@dBKzzSOQDv{-yp?* z0=c{B5>JuaB^K~In+813y9j?ils*7*$`qJsU;^e&K0Mh~Kl_7qHNFg!^7&4k8Vv?H zbGr2uLuttI_s#|f#lg%`(*}0)MaHIW5si#5v$u5=4G2!zK70YtBy=`vS0sdk~ZZI&+z5*z;LUtz4LZ zfFhFbshUpm~zZOp%=- zQod}&PZq0n77%CH85Lp=*~RFhg+qslWH#XMhM8*(uVVCHL%LB-WVVrwC! zXPvKpKY!~9270SDSKQo+54$Io2cvC{i@QB0pxo ze?xU4Md3Xe(X3^glQd!3c%YC3$Ru2Wkb<9Kbp!F5+w)Iq<6ve7=f(Gx5 z@fk+JiUfCN(2Ziwqi{;k9|O#{1ReZN(ss$;CAYzfjU0NX+fzyy}LN$ z`Nq4RY1?&w;Pc1{mYk7BOvz^*u@wdN%;_);T3($>n6|XD^=Fvx0h;y^TQOl;9+}!1C@%V zWvKzri539Xd_f0_tDd-DOe0NV1#)Pew{|l3NqBpA ztOxy@FNQ7`gnpn3yCNk*$z&Vv;!YXcy>+}z#ddq;%)ZYcf%n@g^&KL)+SwPsKJAgV zXM9Au%xr$iww=+M(~fsF<476HB1_x%U5O6ei*;=j^k-6H=dt*m zwD4MPh()?Vb*vnT-sX>KMOUI{o_jp~(n>r|0Y~f4g}m7`xlDy@~a*2fG+d+L@V1 zz4_zzLXTytoH3M1z8kC7#Kxu18@=Sm4Y8nmG`-E+zYDN-SyCcV+q$67+6{Q`ZD7^z zsTy;^njEed;RWdeaa<-kr`P?$wVSE0p1zRFivj-7xbe!R4vHj#$azBE2kz7EYYHhq zgx86!uZ2ART$&hlL^iQbGt;52>R0HcN8$|X@l1}MoJ|XDi0}fiTu1N!BaIMnNI)2`}1NdHGwN7kiIK3w$ z*6M7Z9dr>8kOF|Kuy35#C>cvi{AX*VUstPkU-(R~n#>iyFq%vjh5D#GjtGBLq@ZbG ziqt=`yyoIEnE^F>k^GA|)2v5qrS$amWmJG2`uO#Z6t2>X9?~Z>yW!znJaoMOKfJTX zM!h-yVqz2X3ywK8hXt~l{3U@vLpfx@z~EjOgt$(#!*4)wyPi~|!z&I{S{^|=nZ)&K z4d{9fQo_}Iod3^^d1UVG6f6kW^frGe1dCNQ-K^A2#@^&@<0o{|$zWl$ui<+o5DQfD zrW?0X$@;>XJYX{xXh8g#pi%L2A6Kvp%Iy6DjDfYfP4Dj0%nq-;{r>-RhmXu7E82As$% zq77iUI38#vcHW%~C5QR)_Q7(V(D(^%^qAK|hH6_IN+9HMi(n%-PRf79|sNblk0z|azdm)DDnHY9=v10)Qk_6zw zoT9c|0uet^d)oTF;hfq7n|!8YeNSU*K>2(?YVH+LNj7XEmZggON9{R|chl+>>rd7{ z#f`e|Kq1&y1RRB?SLf^GZGG}et7l~`cXno@ z?RGY+l}&EnA9H8uFKlvENzyA=x{9#}0k~8}6wjHA$FevF{IDhuQAaj&f#g(x0Q{A>tlZLM=-zoOYo3vwIzdTm|PC zAI%%`o*f2?a-l#x*%!lOaa=bfLL|;5+Awy1{%-w$*8XzAg3^y_R>Gb~w>tI)96*zp zXV1;EFdRV;7q@}VC8pjq_atpQ%w_K8G1Xep#r~ioxGWmRXTB+!C)>SO@{-3G1 zLFJbGOr7kCUc8}trm~<{pEc+7JP|bICo|z`b%+-@us-Mh0(Xv~+DmLW12R(TNDZRE zdz*Gr@a1iA`~HHD^LK5sJU1|X3u+G`WEXaL-xV37Zh|2e``e4L=WNFhooQut+u0iq z=!53(f#v@6E7;CmVswoag^{%b9UL_E7+Ha1h&+LV%{q;XkDTS?z4a|=_ZcvMjAsCt zaTo0;a`@mA0^wM~$>9tUV;{i_8+7$X*tq(WfdEGkdobAqwvYYU-+BDa-@<`F4$D8! z&7X+u56XCT7C%GzzO!I{4^ofwnE?L+N$1wJ5mo_J(CgEG>%V$glRC8U#ax$ZjHmcj z_L!;7Z3o&gkNDq|C8>As5773CK^Lpk5U!H_!N_OD*7fD>Kf37oq<^~TKToS3*YP;v z>`-B+SG|E7{9nOyZP(AI$8#Y;a*F?0$foz*2nE&*x;4Y4{#siLBp#4A(pF9DVExa& zG7ME7A(c%|wRic9_sOw*I+)Dq;&UyNSU)8ZB7MSjVRX;-BHuM^n_dljJKtn+ zgeIC5@)95u0=l6vLA7%#B-f*DsbO_m; zCjY{i!@@+ulgaR8f_!_UE9+aXcdqEgn0GPicF-wFbuk=od_%2{n#4mM2`ICXHi%NR ze5aDlk83^Zu#rWEJXL;fbzSQr({ug@E1h;JhwwSV`Zy44#-ZP9Rt*tro_DknC5aUR zoTH)L7Y6)K#Eoa5l?_iP^w%2tKZr5eh%7WAyqjBQ(H9aWENN5g9l$ zL`w8`c`ecQpV8(^uGnJ#Ulv$a9ZPcM_X=kwAO9&ipr=0NUHqiAOX`apFn`gmo8Eq# zw|7}yCH}Ds51`r{bKKl{+0fAN9^6EtcKN(zEZ#KPj-Kc`= zR@G6188C0%Gd}H!GB*V!8&SWb8u3>q+Uk(r<{dG;4fypON7?iYhEL#@anNdLn%={x z(IMt{LcTTv%~Csre?g0V1PUj|7AL4q(hP`>{Xgxc)xU6|aD5s*H2N1KUAA8aRe=9s zi{v_O?lX>?w(`>x5YRCPiC$7q5sWVYC`yBoG7t?BaG6)^qR;c8l(1qZF%)j_d)h^g zgKC+jNir)LEl*S>)Z)+F&&S=eufv2t5e<4R4rBV`pE&?RTtPmTB;_;9<+!kQF%|2c zbak79e5>6qs%oiP^9NpzoLFO9MkN2Y(6&+@P*$r}XhIoGh$=%$-a;v0 zPm_X`Bo^ksMX+c;Ilkh9Y3R4kT0;!tnX~k+^KZl5e5>sDRkd%u2K`Z~MxOsk1TWXg zUe9SxZ~GBfPa_grO^I|(uK$qS%Ya2h=m&cmCB7D0jSl4rYw2@_Dk`Cnui%?G{0=lC zkw%1qf`Xg&JjeqSzrR^DN`LJnffg0|nkDEBYI8+AIe%!VX&U@za+o{lZ}RttH2>cp z{3FTyH}(A6*B|W51yycu6igqNzw@SwaZF1mw)s<}9HGDFJ-5&6W%w;Q^t71j#2*UF z)G58%@455v8s{&@)-V^PQM~!h0pU+yTJ@P07uNj;W{M@C0g2s6q+A@wHu$j&-mafP z!^R(cCGTN~JwKFO`A1eM{L-A=u+hE0v5i~vMNDY&@UHNqyJxltXXwD-5quQ9pxk{6 z^jVilBw?xrvcntpJuJDXn5Oxty_$;6fArInfq!5eu>WqIO&Fi_!m*+!)Zg@0`XuvC z5enx?HRMHJ9G`nqn2xL~vvsq5ghkr4Oc0C+-myoARYom0gjC--Oc_D)d+++^8NqjH zb*6^+t}y@=jm9vvaJ{(|i__X~Snc?Y_o5vumyHT^X+5z{g`a**yZw5-V%3^DO&jZ+J&`NiKW38UX*hnO~3UCZ~D=X*03ArdBw=H8?@|EAg@ zjH<27XnG`hP%YEA?Y9T_qjz2%-Gh%p$*3;W$|p+M5eC)o%93Z^8p+MjaDU<+LzS#!YU1 z2l3d?Ukwn>ggg@r?K!z>R@3|UvZ%VT8?u{>djW!c5>vZMi{JGp;n%rK8zJH7-t5Tz zJUw)Vd#Qt=A*>fbh`7kjTk7Jd2E{4ftHZupKtZYWI)F$bI=B>Wus9`}hSqkC^g0#_ z!fvVWTF!XP9W+iyjR(W^8VO~q*P(w>FKc;Y-<|!VV+lVfh*Ffg-|mtxc@_pbb5aJBl=Nw>6{394X+)odh#G9*3%QYZeN zLu*+(K|`sr+7{@Zc+_>V7%xkF*)(b@f(RgsBt8Ct<82pn{aiUYZ1{9vg6MFNp7|HS>k>l%`aQ?PHC@Is_Fo%yR3xh=xo$q$}zTxQrr+Wl_nymOMdntxfBh<#4W}YO-3p2`bM(4W{da(vm+YmB6P} z)?K{(ebyB76hu-t^zzV0$(q{XZMyf}sD0_NcaRS`nA2>Dhtjf_4S&*h_`RID1T36U z$P-g3HK4E}H^ozgE~5W7nzNiOp5iP!>eYYE=gJUhJ>8K6p??zc+$wN$+*Y{i(6Jrl zhPyF<7@AgSSGCbZCMfmDI}}7`*T~dzRX^OO5Z2eOhsz-R5M5}qvRo95*KjoXnYh+} zo3uhofsPt|H09#(Oq?up&8?L}z3lgqgFJRT*6%{JqULjTbK(5K1CsSBG$*ngkomGS#g~hzbAg?itGff6S@2nTwm3{LP-`5ZQCfJ0e2SbDoOKAg%n{EGg@1rPt4C319Av<~?^<%9F)rww zGH1qf97u+WxKgixX<;a@)kazqQE%*Gm8hN&Bl zo6UEJ2hu|*VosSw%BW^|`-&;=^kd}7qUW*LXU*2wRR>eZ#P@OYhw@KE(o;zHULj{0 zrenM-*k2bFZdc*YPL`Gy9sLzPnV@J99_cXQoaw`=^f6gb|A2keeJ298kRr~X^3!<+ z4+*(>S-f67te0d5Lvc;sro_@TIru!6df}=h_|6m~Lc3>M{L2o*KXxrQlIO9%*rm|LO4nHL)Rc!=8zsgh! z8S8ju$}BKF7(=x$(&%uji7)p%5SFHZvHV(I<~F=Gx_(>Ej87>E>4wkQcSgm~0idcA z^gbe(vtzw8WqZvT|KrC*U*o3!O-^`oVpYvi+E(KciBu_O#dm=TQi?XWsA4P_NJc}fL27D$@Asf$f~fq8Tn)z z--%L6Jl=eY8`T|w=Q%yvTM?gG2)CI>Uaf(6@mhj-A?){ z3j}JLCa%q2iFxMr#5r!FPYvX%GrZMM$b7snoMg1_q7Uca(rG-f670G9Dj7pjY%%q? zw|B;rA`|tTqnu6)k|Wv9x@Yir3KA|^1U@0RVA#i!Wo$9;%Sab=9-h^He?eo zmF-64F1MPY&pfH+$IiNZ#vrBgw+Q)Bg_x2ru3=RkPj%-~edzh+%pZ`Y)$GnJOo#JW zIrQ|(QY>)gx`2G*BP;HYHMwe1+O_5&?~>$f##td-#P%vaBm{%R9WEgjr1j>Ewc~kN zh<$6jc0xHPCs}hrr7K4s{3{8l5HG1vltULVxLf{_D4=jpR)7&yGUZC>lqHLQ=x$Ns z54Q|KlUoPlilybySLXJmp4DvThgUc6k0c6#7qW`Q?C`+QMK)dDJNuEmTBJv{h|p#h^E$FG=XX^9VORXeh~x2&bY^oJFrL7V?8!8m8rm2 zkFTU4s`?(=mR_n!h0d0&r2MnGMNz}j9>MLl7)MNARZS-q3X93CHlx8J$@12`>W|ls zoWjVMD5eC4w(d8g3DY(`8*>7MnSv*W!{w>EU=g*W+UuX$vwWqqRuNt}x(qopfKhhL z%Z8Ok^&ERuMp6kO+SvZjH{#952g)k3sjNHQvALdU}6;>#-UUBdtO9N!AMDgVeI=6U52;?*}DQRdZ1GeNp?XU9SKjXi&l$0AD zi(#)Si{W&)5dcdZ-O9bMT)RQbpgtt&GmYuQmEJwg`I< zRuS6Et*$>l>7JTpuWZ4qzVCdJ)D(cUTyh--27}XeD$)8d98fv+8cWwPm>)x{Z+M6@ zO+od90FgUGpc1C^80tadEyO1pn^HDfSNsC9=Ey*rr{5q2m1f{O4w%G(WGm!YPsEq) zFKoXN1F8xJO3z`7;t|xe^_lXnA*{yUD4MU-OZBVScUIDQ-3OR(#FoGv7(tN%&duC zqUcv|N@L3qull~IOpOdJ2cA?1epp?MCt9k5uqwZCn=6Ty2PH~#bE?=xd{ZBS1aO`N z`u(m~FB|x)mro@3*&qO)<)i5^zgbN=&GESbO(7C3LH;9ho!lcgaL9lFv!7h z-3{NVoS=I)D^a+^3%2K_5P;9;&+Rb>NI62A&YT~d445@SXLVx`&qH| zO}cTJ$!7uY^Ld?K;}e8nx3ME3iSql|0>SpqC~J7eyb<%BO2PDiRn=i7m0Dv@lw{%5L zZi&bV6fpUW$~{b@bS>bD=@dCC>y!rI!B6d2H-ozk}j46Na z_X)3rg7Gh(@vOGX>4jf`O-b~tF1qWVl(qVRHsThqNQYDX;ySsp>vefV3Fq-@!IDK4 z?o*y^Ofa_KR#tMG#TWeUQEa8?MCLJKd-e~T;EhDP)Re=#ypC&j^ny5vmXteII?ZMY zXS8W^_;rdx#zm4kMWTsYK9 z1S2d$Uk7Q2m^!gzMX&^;5Omym^^khF4vgb`xAtvbsw$Uk6PQ-2qqDu{RqK{{>A=*S0a*KPdW>*c;MN_}4{sVVg<`Kv`tBsNVKsNL~stnZF`WSjL+?TpObmd2eHxa5x5`FvE?hEAhm)W8!TxneJlACq`? z+QQr=_dv~mn@9-Q>0iI1EhApFsxSE&E3MbHa_vH8(8r7e6q@oB@^qqPt03o(kcmA% z&3-MpXeQrA0Z#54wKDPS+@l%f>?CoT{9SX}FlZH(F`BG$%|8dDfjv zNHw>nD;C}1W{{hbrGM+~Cqk$7@b)?wpQh(E*MV=fwo_3549LlOs|UX$A(Zz$U?y}` z57+)XGdQAs`VPc7;lrbykvQAVN9oqIG0g$Q=QSVfTEmnfl~K7mC8!Ho{hV72y>?0L zj!R7>aBY?9IslQxIj{Kdr|n^YR}n$)>ip(ZN00J$$(7|WI9<4AUql!{0MFH7DM!7^ zg2n&|ID2=@=-gy5QB$;^Ha|-5A*(B$GX#M?GIz8~Dlne-t_~#laq;l?n)OXx-o_pw zhkmN^t-OtuaNA-MvZ-(WHIm3FX^)4el0!={ z`08n{`hLS1*C-6pF&8_oD6DlDvfCb&&F~9_iOl3AsEO>HxxvWOICLbH?4r-%C+42< z_?v9Si=?dYA~h*`7ncgrO1%(A>nMQQK(i%=vuwK@pX}9K&FP-zhMf?oRNH9E@kcFv ziu}4~eDk!B-B&9Y%hH=SzD^}1E+VBT>DXQ1(=10|_<~Vs z(16KP6-YQyZoZ7~lfCXc;5MG|C-X(Pu!!q8@*4(EMQkW_ROG`_B?aYAp<|+@M;IM= zKlcjAaZQ(QmrLs3cO#>G0cpxEp8UQWf~%*yF)!(-pe;lwY0wZaxc+k8ly8ZWfqQs@ zS84~Ejk8Ilh>t}MW8~LSNjEQ-%V*0%T2)Mmp0MV|6R33F?#~UONP1~ee0?X9I(q-c z&h*mPl*GPDQ`(%U7732a6N?gY60SHbeEr5aT+##&L0ha)+;ZyUu!;};`R1PEV;G&e zj|>1n+i4H)Hpx3Bmt}aQWJCAv9B|M_ux$SQsg_br62!u4wYRypc->d4F!WUA#Pufe z!FJ|{dHLOVytS&kni^g9u;9(j`%D(SW}Fdu*{HU)4;#<~GF)-%1g|f#Gpj51D_d6g z>s4^#rT&aDCqx`8f%9vy5<+p8DZjcRM>by|?9%oK%hE)#-Yl(O$(la0WRocbQeEzH z)G-{~M@!-KKOXoGg~yp#Qr82cneMw4yyzg)_&y3-DXhrn=byynYIC89?_HFoH9E zShitz$uJ-85D9teG+{S;DG^{)ZK?|I$Xd8(0p$IfD=q{<@it&2`XQ4zgXqLyu!1L> zDUbkJ$s$oU=AHPj^VHAdf`Cg5ABLePkD(-oZ;AG=_$WJ{aUB%EDrJ6 zDP!ltitED7ev^r;S(E)@OncG+eRtxeus#_UMtd4ADj1|Y0w!8c69%L*AYsCn(6*%; zXM$$()1ZnkC9U1cL%(c}KF;6Th>H`~9%-rR0!9H2KGYM;tHYTZGz~fD*1fayhC<$| z;s6R}H?IwlS~XTIVC!1UkA$A&D!wTOJci*LIRkV}5JCer8t0@Hm+6kL{h{ULSFiFu zzQ*em%!{IvicDR-LmP0?0Gc)-)9G)FzrJ>DF9JRTEA`t!I@rZKVH=pZ)@}m=oc%_nd96I;r-xV;&x&xfFk^u$Sgd2IHMs$J+sY zR@&DucHwf&%?jax1lE@()T@s~@|YUBl>fz-)YCBDX+dUCWsTq@6!6?VhU5%~QbN)t z#Yu|(D_~j|OJ%=tY`eW*QWD5`PbO{rI0ASwUXA}|_E0WeEoASnKY^W4@Lz;W?&=Mm z_z>IaBEcmzgVjl8R+Kc2s)TCUnG{Vw(ir0Eb^wNh74la~MGo#(F7rC48u zjX@!EdB#jEV4@^D|BPt(t@X|+!!LDrBxuAU>6A(e?iU0WM{utw%$EC;gz|l)syvisF&?xaVWz^j zlQa%Bx(CYcq5Btdb{2@JySv#XC{Y(YP|}=#X9j>@Qycv(G<{ahfllkwY4@3QDwdx2 zeLR*W(XF%IV?c_l=Ru9J9J9HG9GNtd^U-d@wMFi?#Ph9`wwAN98t^75GJuS@sCs2G zHZ!M!ivPH#VpT(2JkNS^@mW>UNKA{HD#4$+LQ|F^?l^o#*=<%n#gE zD4A9B>-jh3n0KDv1o%12PS$OabT~pRY~*f$$0qP9=QLF7w%_#hP{6@?ll|aGN;Xa$ zplABC!=hm(3WqtA+B`7BBqA6?<@{V%l70`wsKb$*J6qQ69{nmaGwnMzGGSPW6;QR% z)Lu(LU7T3rC^VqG_52N1!UAQz0l#PZ@Ni8fKTB%`v+%4+B6Fmm2mO4LVleH{)r;ykkxe&40((hn6&&^mWDx0713J2c*_viiS&QV6%i z+5Hyk`KADsY)qPG!U&$p;aGV094P@VU^t9q#+T*-7CtqixVAunMxcM>pH#S4d&?x^}2*J-v6 z<3$j1-D)W~t?)V@>o{IKMdffw2@R+M$X*-`5&KgJEl$l}z}v6_{Gz_(LWp(di^00` zP0v@@%FAX^&0#Wg%Bv`ik8;&T1da=c>$w{8ht=e`CZU!-I)#O^F^Jn$ogSa5#EsZLZP>W|NhY&{AL^cFXP^B zP=I|3vvD=Z2* z0>4|n(-Q1~qNCGr0KgG@3m(|UP zK!`3a=)=Mpi%3~1Dq*%TFFxf`^?xX}wm=-EsgEEs{6?lwvXamdqr*!tJL7-VC)CC! zTr9b>E)=&$d3_vVXw3ltysmGmcsq186H4bpk(Vwxk_Q)l44j>AK{Ed*O);&dm5-OQ z2ysHBWR1~Mgbam=-x_!6YP;N?*5LNn=`;^+BRE6DTa5+_-kuZ%4An12fjV{mvs#7*j5m|DD1aun5tBx~MC1fp5d zMv|P5$|x5>&@ya=C+dFLS4R2+SGeigCK2zv__)msY}?IW8V^(xTD%)diLyzlu1rdN zVsck!TX$cb?=UA$X>U-iblN}URph8c0z@KYA55A;Fe2HA%=kM(NYoEL9)QF_1I(o0 z8#rrTE~A_t%$GyBEORx+4%uN_LD&Z(U~)?M!h2ErB)ehPe>N+nPXBq_ZsA!RTT+A2*^1$_?QcR zcZ2ekSb}?|&uahFca7DH%Lh{87$XYJX=O7oCW#1HOjK1d;!bscDq)b))D(?9GlAMQ zq!q6esPs~U|E%ujI|~{ zJT&vK%k931CxX8N1Jx?m{WRi7!eXNKYfTYIm1_<&LtKI-Db z=FC*VMgK-z+Ie5LuJ~^bafqLqUybtzC+wsBA4(MeJ<^1lgny8{PZ zwKfoiTrnsiWjCPO6z@a_!=Ky?&2P@^w2$7)1%7jXyYBYHi1lglK5euPbK9%4 z#ZHO*4TJoC3tjYY;DQZ0menZg@Z0inU6hYba1nVoj%@-7&N6j$*TUIK#01wt-pk>A z>D}9rabJ~^8ZYwuZsU=<>%Dg?-FP%LQGT3EZJ2M0BJq9KV|uSlN5Yp{oGO|ht911+ zu?YZh^_Xm&Ue{KpYqL`-E+%)Gd@-=-9icO%#AiMc)kM4q#9IO}N-Y1ATV*B&L)q1xgT>mmk&W zRU~^=XCc`n%1@~4JQxf+mNw4y1DxN!c0J+s<3`86d$sJ* zyHUWt?s|q^p^(b4gQ@arJv{4dN$5!C=8R$0hkj3#dP$u+LO593(CWq;(fMTEvZ)9- zx?0hqG`5M4#M@B^;&XMa=7hZU5%Fuf02-a^X&LYQdm9^U0);|X*ugRRenT8tHjc_O z4X12lZI#I@+vr^-I+wP|fQQ)?5ZpDaQ^A=*_9O!gR+F111mI#e@CUqA=3fsA zN;SRBmebYf2itG=oV@uTf-_`s=I%JMXNc^muO|3IhVDj;P#aLRVf5Ut!10j4oKimgJ9`sEm*Vd7`9l`Uvf47;icj;H6 z=DG9dQAKVh2-RGYu|reUHw$`)3^Kfx!vcU_Qta@ zopo2RJ0HEFdU4e3GB@ba#kRbI5z8D{E%;FQxVID__R6=K7Zpt@L^X8IUUA}iBs7V~ zzi}WEaynvo^W}J)_8wS%2p#o^HlAb+(BaTBasy)0{Tb4aVpU43OUy2C7C}F%7ue%^ zadM+ES1=%Ey0#AMqQ5L6yuu8% z_^EV3eYuEd{R|tZr06I@vLMrz4Dn=v>w=JlQ6ugLa!Z~Gppoa8&J$xIApDE z_&PPEBi_BL{!2NvW7(v-zEo(WmOIhQ+^|39FW|;u<(JmMrA0rLNe-;XBW5-;KHO|S z*Zul2FElXlW^9nTmND*XyU-VPiH%M^?xm9c!7InJQumsz4#qj=e&Nw5F4|!;8NazE z}I^TBaj{R{ZlWY%|2m!o{y5Ql?x8zu}|^0#t5Y)TsJY^Gnhh+I+d zVpOV~SsJDlP3%kSoXeZwROeEM1w{8<>gLGC;L7Ev832W+EV7g^QBu9ksMc3+5SH? zsq(!{?r;m<3+3MNN$wWm7=fa>^4-};3GElA z@Rj2qaj+?c6c}X}XhOtkTw*J{?Y@8ZLXCzBi(P(Quu)oG*ZS2V@%fo1mHsE6w68^w zJ5V9>w#TQI{TSL-JX%vkR7qZU_bsL0y@Dq^Vx>a?&fjryFjE@hrOK3eKT9(Jf~*C* zZIG!^(81Z1YKB||vlR@cTbtSo$Pu-cG+4n;ms$#VAI?#}LRe4U3itu|N!FYrXJ(Wj zoY2A@r8#3CGfOeg2v+Pd~9=ivF@?xHtBBl}=^v7Wgwa29QeT2jgtJ?bsL~Ord@3&D21u%B~&iFOuRP z&F~D5ZF57`w5i+QPb5CCSh?iBJ zR+V(<(GJ+VLsM1GUb7FE_E~N=Iv|^l8t18LSP0)XrH=D8wbbgZYsCNB{2JcgTn`zn zx}uy#@2;uZYTyrdBa>;FKv}Qje+BH0W?AV-kHL2(TG##MafPM8gFxRj)Zz9e96Xlihu*j zib9E4lHgDWDCz>gEi^0`$3&barm-ph831tGqg!ZfWN4OB^YHrxq#!K$`LrGvzu}`G z_}i_i*I1M7G&M=NcY53V<~Gj{Jk82FH{lcN;)p>?=?qRXB3r2Y@H+)S@@j($J_R6% zd)BXHH7XlD`+g3C(BlzMgnx&J5$cZzyGEfIwtNQlMeB2glV0Bs>iSb zQH6cA|7i7nPj`tZ=!5kS<+$NTf$;k zln>WX1-?wj#Ydw05Tt&ObKYtsVaV}|jTsoXNWRp&6tyYX-4%|s1x-2h>Zu6H3Z)m@ zT+?J0cHzX=gN^R~uG7YOdg(r)MpK-#M==lK;djz2ScSQFXQnJ+hLh9n152W2x62K6 z`p=QzIhYI`axgLX-=AQF$SBAahVN)!XrgrOKDb*9Z8mWVH`^d~4C@KWK%A4=ERr-? z65@kC0E~^b7*}hbH8^$fJIcM=cSWjW^w|AD(02Jrk9_CtI}BOGAV7YTMo7B1XxQe; zE{$T#xzG3tUS+#OTwKoj-I*L`*81Y|%Yl!POTRlOdj!RVz&C9`wypD> zhodL|Q_@vo@SH%#UFzXWf_4bqQL3$)X-CdmN6N)9jeW=1H{6x zAMvMNcNyaunzH8E)Ud}sedmM;@iVy~a2hF@i0x&d%g_1>%smp~wNI~@lsA*i=5IfJ z=eg-ORhxQi$x~pHz_0p&lIbe$`S}c)G%k8x^fQ){=HT~k{G8S)M# zBwdzmtRNXFt6IY;l5WaLk3h?&xLm`Yt2$}YDY_sLDH}nq9{|i%mVH;hYXmg7bI8w= zL5^_bXy(hgSp77mDZ~^@K*>y}?LV#@in9%D0~JbORJEkD-sBvD>56u*8EMO&{q7J7 zxN1i4F=RiYFu0nA*dx`BULa4+dWKldHJ|g4Vi4*WY%8JpMDl#SSkezIj(0P;te+6j zgmKbz*S}$W7U9`Awu#aY!fa$$jP`H1HxCX6fU@#4QO=IM78spY67na%6F6AFM2lSL zT@9q(4mCOiGJQz$Ga5^2u%P@s3iTE490?t)^M{=eHct^BMbuBb)|ywc-p*I_TP5sb zs`Fmi{fv%cvf^QiBp3s0Oei6GN0GI{$t-@R1drA|F=N>VMluRZK5C*Zjb%%gL%H+V zQ?A+zt>a6q-K}4q?7I<`ukSW_vP!?O!p-<$=S^g*EIMCWWkrS}YDLznB@9_=*^%u2 z8%z)PJG8YS?088}4y}Lx)>hGxyjaW?G&@7SyiO;so7&()UDN0B@fBRrIe|a5IPvl< z%$pDlSjD;UtPqPapxz)}b-M_QCSowcdVlrQuRrbMR&TYGEl%sI$4SMX-UZLt>|wp0!pi7L-g`-zixRU!>W^o~dy z5YcPsJJON{H*1=F@T3ZzO1na{uaDTQtjXU~NF~-`ru@=O$7x*L(ih6^9VSrq1)mQ1eczdL#xk=`3CobIBd$E;5 z;WW7N)RmD87#&IyG!MuMhe_c`QkM*)xUFq+8|E*VEo$T{qh0rzlgA$mw)ta;2RN=n zBY+L>xCz+8#AzNVufh80c~J3�-&?BSD9?pvD_MX-TG67ncWO>`V_T&yf*>@3FI` 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 RfV&@nf{dzkg`|1Ne*xxbTTlQ1 literal 0 HcmV?d00001 diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java index 831ede9b..d2740094 100644 --- a/src/com/gitblit/ConfigUserService.java +++ b/src/com/gitblit/ConfigUserService.java @@ -33,6 +33,7 @@ import org.eclipse.jgit.util.FS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessPermission; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.utils.ArrayUtils; @@ -267,6 +268,55 @@ public class ConfigUserService implements IUserService { return updateUserModel(model.username, model); } + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(List models) { + try { + read(); + for (UserModel model : models) { + UserModel originalUser = users.remove(model.username.toLowerCase()); + users.put(model.username.toLowerCase(), model); + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name.toLowerCase()); + if (t == null) { + // new team + team.addUser(model.username); + teams.put(team.name.toLowerCase(), team); + } else { + // do not clobber existing team definition + // maybe because this is a federated user + t.addUser(model.username); + } + } + + // check for implicit team removal + if (originalUser != null) { + for (TeamModel team : originalUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(model.username); + } + } + } + } + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()), + t); + } + return false; + } + /** * Updates/writes and replaces a complete user object keyed by username. * This method allows for renaming a user. @@ -413,7 +463,7 @@ public class ConfigUserService implements IUserService { read(); for (Map.Entry entry : teams.entrySet()) { TeamModel model = entry.getValue(); - if (model.hasRepository(role)) { + if (model.hasRepositoryPermission(role)) { list.add(model.name); } } @@ -447,10 +497,10 @@ public class ConfigUserService implements IUserService { for (TeamModel team : teams.values()) { // team has role, check against revised team list if (specifiedTeams.contains(team.name.toLowerCase())) { - team.addRepository(role); + team.addRepositoryPermission(role); } else { // remove role from team - team.removeRepository(role); + team.removeRepositoryPermission(role); } } @@ -494,6 +544,28 @@ public class ConfigUserService implements IUserService { return updateTeamModel(model.name, model); } + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateTeamModels(List models) { + try { + read(); + for (TeamModel team : models) { + teams.put(team.name.toLowerCase(), team); + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t); + } + return false; + } + /** * Updates/writes and replaces a complete team object keyed by teamname. * This method allows for renaming a team. @@ -602,7 +674,7 @@ public class ConfigUserService implements IUserService { read(); for (Map.Entry entry : users.entrySet()) { UserModel model = entry.getValue(); - if (model.hasRepository(role)) { + if (model.hasRepositoryPermission(role)) { list.add(model.username); } } @@ -623,6 +695,7 @@ public class ConfigUserService implements IUserService { * @return true if successful */ @Override + @Deprecated public boolean setUsernamesForRepositoryRole(String role, List usernames) { try { Set specifiedUsers = new HashSet(); @@ -636,10 +709,10 @@ public class ConfigUserService implements IUserService { for (UserModel user : users.values()) { // user has role, check against revised user list if (specifiedUsers.contains(user.username.toLowerCase())) { - user.addRepository(role); + user.addRepositoryPermission(role); } else { // remove role from user - user.removeRepository(role); + user.removeRepositoryPermission(role); } } @@ -665,17 +738,17 @@ public class ConfigUserService implements IUserService { read(); // identify users which require role rename for (UserModel model : users.values()) { - if (model.hasRepository(oldRole)) { - model.removeRepository(oldRole); - model.addRepository(newRole); + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); } } // identify teams which require role rename for (TeamModel model : teams.values()) { - if (model.hasRepository(oldRole)) { - model.removeRepository(oldRole); - model.addRepository(newRole); + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); } } // persist changes @@ -701,12 +774,12 @@ public class ConfigUserService implements IUserService { // identify users which require role rename for (UserModel user : users.values()) { - user.removeRepository(role); + user.removeRepositoryPermission(role); } // identify teams which require role rename for (TeamModel team : teams.values()) { - team.removeRepository(role); + team.removeRepositoryPermission(role); } // persist changes @@ -768,21 +841,44 @@ public class ConfigUserService implements IUserService { config.setStringList(USER, model.username, ROLE, roles); // repository memberships - // null check on "final" repositories because JSON-sourced UserModel - // can have a null repositories object - if (!ArrayUtils.isEmpty(model.repositories)) { - config.setStringList(USER, model.username, REPOSITORY, new ArrayList( - model.repositories)); + if (model.permissions == null) { + // null check on "final" repositories because JSON-sourced UserModel + // can have a null repositories object + if (!ArrayUtils.isEmpty(model.repositories)) { + config.setStringList(USER, model.username, REPOSITORY, new ArrayList( + model.repositories)); + } + } else { + // discrete repository permissions + List permissions = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + permissions.add(entry.getValue().asRole(entry.getKey())); + } + } + config.setStringList(USER, model.username, REPOSITORY, permissions); } } // write teams for (TeamModel model : teams.values()) { - // null check on "final" repositories because JSON-sourced TeamModel - // can have a null repositories object - if (!ArrayUtils.isEmpty(model.repositories)) { - config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList( - model.repositories)); + if (model.permissions == null) { + // null check on "final" repositories because JSON-sourced TeamModel + // can have a null repositories object + if (!ArrayUtils.isEmpty(model.repositories)) { + config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList( + model.repositories)); + } + } else { + // discrete repository permissions + List permissions = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + permissions.add(entry.getValue().asRole(entry.getKey())); + } + } + config.setStringList(TEAM, model.name, REPOSITORY, permissions); } // null check on "final" users because JSON-sourced TeamModel @@ -872,7 +968,7 @@ public class ConfigUserService implements IUserService { Set repositories = new HashSet(Arrays.asList(config .getStringList(USER, username, REPOSITORY))); for (String repository : repositories) { - user.addRepository(repository); + user.addRepositoryPermission(repository); } // update cache @@ -886,7 +982,7 @@ public class ConfigUserService implements IUserService { Set teamnames = config.getSubsections(TEAM); for (String teamname : teamnames) { TeamModel team = new TeamModel(teamname); - team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname, + team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname, REPOSITORY))); team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER))); team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname, diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java index c831c42d..ed48bd27 100644 --- a/src/com/gitblit/Constants.java +++ b/src/com/gitblit/Constants.java @@ -15,6 +15,10 @@ */ package com.gitblit; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Constant values used by Gitblit. @@ -309,4 +313,72 @@ public class Constants { return null; } } + + /** + * The access permissions available for a repository. + */ + public static enum AccessPermission { + NONE("N"), VIEW("V"), CLONE("R"), PUSH("RW"), CREATE("RWC"), DELETE("RWD"), REWIND("RW+"); + + public static AccessPermission LEGACY = REWIND; + + public final String code; + + private AccessPermission(String code) { + this.code = code; + } + + public boolean atLeast(AccessPermission perm) { + return ordinal() >= perm.ordinal(); + } + + public boolean exceeds(AccessPermission perm) { + return ordinal() > perm.ordinal(); + } + + public String asRole(String repository) { + return code + ":" + repository; + } + + @Override + public String toString() { + return code; + } + + public static AccessPermission permissionFromRole(String role) { + String [] fields = role.split(":", 2); + if (fields.length == 1) { + // legacy/undefined assume full permissions + return AccessPermission.LEGACY; + } else { + // code:repository + return AccessPermission.fromCode(fields[0]); + } + } + + public static String repositoryFromRole(String role) { + String [] fields = role.split(":", 2); + if (fields.length == 1) { + // legacy/undefined assume full permissions + return role; + } else { + // code:repository + return fields[1]; + } + } + + public static AccessPermission fromCode(String code) { + for (AccessPermission perm : values()) { + if (perm.code.equalsIgnoreCase(code)) { + return perm; + } + } + return AccessPermission.NONE; + } + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + public @interface Unused { + } } diff --git a/src/com/gitblit/DownloadZipFilter.java b/src/com/gitblit/DownloadZipFilter.java index e515b55e..225e5e11 100644 --- a/src/com/gitblit/DownloadZipFilter.java +++ b/src/com/gitblit/DownloadZipFilter.java @@ -91,7 +91,7 @@ public class DownloadZipFilter extends AccessRestrictionFilter { */ @Override protected boolean canAccess(RepositoryModel repository, UserModel user, String action) { - return user.canAccessRepository(repository); + return user.canView(repository); } } diff --git a/src/com/gitblit/FederationPullExecutor.java b/src/com/gitblit/FederationPullExecutor.java index 7b9c55ba..03109dea 100644 --- a/src/com/gitblit/FederationPullExecutor.java +++ b/src/com/gitblit/FederationPullExecutor.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -41,6 +42,7 @@ import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.FederationPullStatus; import com.gitblit.Constants.FederationStrategy; import com.gitblit.GitBlitException.ForbiddenException; @@ -333,10 +335,20 @@ public class FederationPullExecutor implements Runnable { // reparent all repository permissions if the local // repositories are stored within subfolders if (!StringUtils.isEmpty(registrationFolder)) { - List permissions = new ArrayList(user.repositories); - user.repositories.clear(); - for (String permission : permissions) { - user.addRepository(registrationFolder + "/" + permission); + if (user.permissions != null && user.permissions.size() > 0) { + // pulling from >= 1.2 version + Map copy = new HashMap(user.permissions); + user.permissions.clear(); + for (Map.Entry entry : copy.entrySet()) { + user.setRepositoryPermission(registrationFolder + "/" + entry.getKey(), entry.getValue()); + } + } else { + // pulling from <= 1.1 version + List permissions = new ArrayList(user.repositories); + user.repositories.clear(); + for (String permission : permissions) { + user.addRepositoryPermission(registrationFolder + "/" + permission); + } } } @@ -347,8 +359,17 @@ public class FederationPullExecutor implements Runnable { GitBlit.self().updateUserModel(user.username, user, true); } else { // update repository permissions of local user - for (String repository : user.repositories) { - localUser.addRepository(repository); + if (user.permissions != null && user.permissions.size() > 0) { + // pulling from >= 1.2 version + Map copy = new HashMap(user.permissions); + for (Map.Entry entry : copy.entrySet()) { + localUser.setRepositoryPermission(entry.getKey(), entry.getValue()); + } + } else { + // pulling from <= 1.1 version + for (String repository : user.repositories) { + localUser.addRepositoryPermission(repository); + } } localUser.password = user.password; localUser.canAdmin = user.canAdmin; @@ -369,12 +390,16 @@ public class FederationPullExecutor implements Runnable { // update team repositories TeamModel remoteTeam = user.getTeam(teamname); - if (remoteTeam != null && !ArrayUtils.isEmpty(remoteTeam.repositories)) { - int before = team.repositories.size(); - team.addRepositories(remoteTeam.repositories); - int after = team.repositories.size(); - if (after > before) { - // repository count changed, update + if (remoteTeam != null) { + if (remoteTeam.permissions != null) { + // pulling from >= 1.2 + for (Map.Entry entry : remoteTeam.permissions.entrySet()){ + team.setRepositoryPermission(entry.getKey(), entry.getValue()); + } + GitBlit.self().updateTeamModel(teamname, team, false); + } else if(!ArrayUtils.isEmpty(remoteTeam.repositories)) { + // pulling from <= 1.1 + team.addRepositoryPermissions(remoteTeam.repositories); GitBlit.self().updateTeamModel(teamname, team, false); } } diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index f4394696..c06266dc 100644 --- a/src/com/gitblit/FileUserService.java +++ b/src/com/gitblit/FileUserService.java @@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessPermission; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.utils.ArrayUtils; @@ -243,7 +244,7 @@ public class FileUserService extends FileSettings implements IUserService { } break; default: - model.addRepository(role); + model.addRepositoryPermission(role); } } // set the teams for the user @@ -266,6 +267,29 @@ public class FileUserService extends FileSettings implements IUserService { return updateUserModel(model.username, model); } + /** + * Updates/writes all specified user objects. + * + * @param model a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(List models) { + try { + Properties allUsers = read(); + for (UserModel model : models) { + updateUserCache(allUsers, model.username, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()), + t); + } + return false; + } + /** * Updates/writes and replaces a complete user object keyed by username. * This method allows for renaming a user. @@ -280,8 +304,43 @@ public class FileUserService extends FileSettings implements IUserService { public boolean updateUserModel(String username, UserModel model) { try { Properties allUsers = read(); + updateUserCache(allUsers, username, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + private boolean updateUserCache(Properties allUsers, String username, UserModel model) { + try { UserModel oldUser = getUserModel(username); - ArrayList roles = new ArrayList(model.repositories); + List roles; + if (model.permissions == null) { + // legacy, use repository list + roles = new ArrayList(model.repositories); + } else { + // discrete repository permissions + roles = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } // Permissions if (model.canAdmin) { @@ -336,8 +395,6 @@ public class FileUserService extends FileSettings implements IUserService { } } } - - write(allUsers); return true; } catch (Throwable t) { logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), @@ -552,8 +609,8 @@ public class FileUserService extends FileSettings implements IUserService { String[] roles = value.split(","); // skip first value (password) for (int i = 1; i < roles.length; i++) { - String r = roles[i]; - if (r.equalsIgnoreCase(oldRole)) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(oldRole)) { needsRenameRole.add(username); break; } @@ -573,9 +630,13 @@ public class FileUserService extends FileSettings implements IUserService { // skip first value (password) for (int i = 1; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(oldRole)) { - sb.append(value); + String repository = AccessPermission.repositoryFromRole(values[i]); + if (repository.equalsIgnoreCase(oldRole)) { + AccessPermission permission = AccessPermission.permissionFromRole(values[i]); + sb.append(permission.asRole(newRole)); + sb.append(','); + } else { + sb.append(values[i]); sb.append(','); } } @@ -612,9 +673,9 @@ public class FileUserService extends FileSettings implements IUserService { String value = allUsers.getProperty(username); String[] roles = value.split(","); // skip first value (password) - for (int i = 1; i < roles.length; i++) { - String r = roles[i]; - if (r.equalsIgnoreCase(role)) { + for (int i = 1; i < roles.length; i++) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(role)) { needsDeleteRole.add(username); break; } @@ -630,10 +691,10 @@ public class FileUserService extends FileSettings implements IUserService { sb.append(password); sb.append(','); // skip first value (password) - for (int i = 1; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(role)) { - sb.append(value); + for (int i = 1; i < values.length; i++) { + String repository = AccessPermission.repositoryFromRole(values[i]); + if (!repository.equalsIgnoreCase(role)) { + sb.append(values[i]); sb.append(','); } } @@ -722,7 +783,7 @@ public class FileUserService extends FileSettings implements IUserService { repositories.add(role); } } - team.addRepositories(repositories); + team.addRepositoryPermissions(repositories); team.addUsers(users); team.addMailingLists(mailingLists); team.preReceiveScripts.addAll(preReceive); @@ -912,6 +973,27 @@ public class FileUserService extends FileSettings implements IUserService { public boolean updateTeamModel(TeamModel model) { return updateTeamModel(model.name, model); } + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + public boolean updateTeamModels(List models) { + try { + Properties allUsers = read(); + for (TeamModel model : models) { + updateTeamCache(allUsers, model.name, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t); + } + return false; + } /** * Updates/writes and replaces a complete team object keyed by teamname. @@ -939,12 +1021,30 @@ public class FileUserService extends FileSettings implements IUserService { private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) { StringBuilder sb = new StringBuilder(); - if (!ArrayUtils.isEmpty(model.repositories)) { - for (String repository : model.repositories) { - sb.append(repository); - sb.append(','); + List roles; + if (model.permissions == null) { + // legacy, use repository list + if (model.repositories != null) { + roles = new ArrayList(model.repositories); + } else { + roles = new ArrayList(); + } + } else { + // discrete repository permissions + roles = new ArrayList(); + for (Map.Entry entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + roles.add(entry.getValue().asRole(entry.getKey())); + } } } + + for (String role : roles) { + sb.append(role); + sb.append(','); + } + if (!ArrayUtils.isEmpty(model.users)) { for (String user : model.users) { sb.append('!'); diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 7fbd3efd..8c6d9eba 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -69,6 +69,7 @@ import org.eclipse.jgit.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.Constants.AuthorizationControl; import com.gitblit.Constants.FederationRequest; @@ -618,6 +619,7 @@ public class GitBlit implements ServletContextListener { * @param usernames * @return true if successful */ + @Deprecated public boolean setRepositoryUsers(RepositoryModel repository, List repositoryUsers) { return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers); } @@ -699,6 +701,7 @@ public class GitBlit implements ServletContextListener { * @param teamnames * @return true if successful */ + @Deprecated public boolean setRepositoryTeams(RepositoryModel repository, List repositoryTeams) { return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams); } @@ -957,14 +960,13 @@ public class GitBlit implements ServletContextListener { if (model == null) { return null; } - if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) { - if (user != null && user.canAccessRepository(model)) { - return model; - } - return null; - } else { + if (user == null) { + user = UserModel.ANONYMOUS; + } + if (user.canView(model)) { return model; } + return null; } /** @@ -1224,11 +1226,7 @@ public class GitBlit implements ServletContextListener { } model.hasCommits = JGitUtils.hasCommits(r); model.lastChange = JGitUtils.getLastChange(r); - if (repositoryName.indexOf('/') == -1) { - model.projectPath = ""; - } else { - model.projectPath = repositoryName.substring(0, repositoryName.indexOf('/')); - } + model.projectPath = StringUtils.getFirstPathElement(repositoryName); StoredConfig config = r.getConfig(); boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url")); @@ -1449,6 +1447,9 @@ public class GitBlit implements ServletContextListener { */ private void closeRepository(String repositoryName) { Repository repository = getRepository(repositoryName); + if (repository == null) { + return; + } RepositoryCache.close(repository); // assume 2 uses in case reflection fails @@ -1756,7 +1757,7 @@ public class GitBlit implements ServletContextListener { clearRepositoryMetadataCache(repositoryName); RepositoryModel model = removeFromCachedRepositoryList(repositoryName); - if (!ArrayUtils.isEmpty(model.forks)) { + if (model != null && !ArrayUtils.isEmpty(model.forks)) { resetRepositoryListCache(); } @@ -2646,26 +2647,46 @@ public class GitBlit implements ServletContextListener { // create a Gitblit repository model for the clone RepositoryModel cloneModel = repository.cloneAs(cloneName); + // owner has REWIND/RW+ permissions cloneModel.owner = user.username; updateRepositoryModel(cloneName, cloneModel, false); - if (AuthorizationControl.NAMED.equals(cloneModel.authorizationControl)) { - // add the owner of the source repository to the clone's access list - if (!StringUtils.isEmpty(repository.owner)) { - UserModel owner = getUserModel(repository.owner); - if (owner != null) { - owner.repositories.add(cloneName); - updateUserModel(owner.username, owner, false); - } + // add the owner of the source repository to the clone's access list + if (!StringUtils.isEmpty(repository.owner)) { + UserModel originOwner = getUserModel(repository.owner); + if (originOwner != null) { + originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE); + updateUserModel(originOwner.username, originOwner, false); } + } - // inherit origin's access lists - List users = getRepositoryUsers(repository); - setRepositoryUsers(cloneModel, users); + // grant origin's user list clone permission to fork + List users = getRepositoryUsers(repository); + List cloneUsers = new ArrayList(); + for (String name : users) { + if (!name.equalsIgnoreCase(user.username)) { + UserModel cloneUser = getUserModel(name); + if (cloneUser.canClone(repository)) { + // origin user can clone origin, grant clone access to fork + cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE); + } + cloneUsers.add(cloneUser); + } + } + userService.updateUserModels(cloneUsers); - List teams = getRepositoryTeams(repository); - setRepositoryTeams(cloneModel, teams); + // grant origin's team list clone permission to fork + List teams = getRepositoryTeams(repository); + List cloneTeams = new ArrayList(); + for (String name : teams) { + TeamModel cloneTeam = getTeamModel(name); + if (cloneTeam.canClone(repository)) { + // origin team can clone origin, grant clone access to fork + cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE); + } + cloneTeams.add(cloneTeam); } + userService.updateTeamModels(cloneTeams); // add this clone to the cached model addToCachedRepositoryList(cloneModel); diff --git a/src/com/gitblit/GitFilter.java b/src/com/gitblit/GitFilter.java index 8ce4d3a7..cfe4fe3f 100644 --- a/src/com/gitblit/GitFilter.java +++ b/src/com/gitblit/GitFilter.java @@ -147,33 +147,25 @@ public class GitFilter extends AccessRestrictionFilter { // Git Servlet disabled return false; } - boolean readOnly = repository.isFrozen; - if (readOnly || repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) { - boolean authorizedUser = user.canAccessRepository(repository); - if (action.equals(gitReceivePack)) { - // Push request - if (!readOnly && authorizedUser) { - // clone-restricted or push-authorized - return true; - } else { - // user is unauthorized to push to this repository - logger.warn(MessageFormat.format("user {0} is not authorized to push to {1}", - user.username, repository)); - return false; - } - } else if (action.equals(gitUploadPack)) { - // Clone request - boolean cloneRestricted = repository.accessRestriction - .atLeast(AccessRestrictionType.CLONE); - if (!cloneRestricted || (cloneRestricted && authorizedUser)) { - // push-restricted or clone-authorized - return true; - } else { - // user is unauthorized to clone this repository - logger.warn(MessageFormat.format("user {0} is not authorized to clone {1}", - user.username, repository)); - return false; - } + if (action.equals(gitReceivePack)) { + // Push request + if (user.canPush(repository)) { + return true; + } else { + // user is unauthorized to push to this repository + logger.warn(MessageFormat.format("user {0} is not authorized to push to {1}", + user.username, repository)); + return false; + } + } else if (action.equals(gitUploadPack)) { + // Clone request + if (user.canClone(repository)) { + return true; + } else { + // user is unauthorized to clone this repository + logger.warn(MessageFormat.format("user {0} is not authorized to clone {1}", + user.username, repository)); + return false; } } return true; diff --git a/src/com/gitblit/GitServlet.java b/src/com/gitblit/GitServlet.java index 2571693d..8e2326d4 100644 --- a/src/com/gitblit/GitServlet.java +++ b/src/com/gitblit/GitServlet.java @@ -105,6 +105,21 @@ public class GitServlet extends org.eclipse.jgit.http.server.GitServlet { ReceivePack rp = super.create(req, db); rp.setPreReceiveHook(hook); rp.setPostReceiveHook(hook); + + // determine pushing user + PersonIdent person = rp.getRefLogIdent(); + UserModel user = GitBlit.self().getUserModel(person.getName()); + if (user == null) { + // anonymous push, create a temporary usermodel + user = new UserModel(person.getName()); + } + + // enforce advanced ref permissions + RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName); + rp.setAllowCreates(user.canCreateRef(repository)); + rp.setAllowDeletes(user.canDeleteRef(repository)); + rp.setAllowNonFastForwards(user.canRewindRef(repository)); + return rp; } }); @@ -209,7 +224,25 @@ public class GitServlet extends org.eclipse.jgit.http.server.GitServlet { scripts.addAll(repository.postReceiveScripts); UserModel user = getUserModel(rp); runGroovy(repository, user, commands, rp, scripts); - + for (ReceiveCommand cmd : commands) { + if (Result.OK.equals(cmd.getResult())) { + // add some logging for important ref changes + switch (cmd.getType()) { + case DELETE: + logger.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name())); + break; + case CREATE: + logger.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name)); + break; + case UPDATE_NONFASTFORWARD: + logger.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name())); + break; + default: + break; + } + } + } + // Experimental // runNativeScript(rp, "hooks/post-receive", commands); } diff --git a/src/com/gitblit/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java index b4640b58..141ad8f1 100644 --- a/src/com/gitblit/GitblitUserService.java +++ b/src/com/gitblit/GitblitUserService.java @@ -167,6 +167,11 @@ public class GitblitUserService implements IUserService { return serviceImpl.updateUserModel(model); } + @Override + public boolean updateUserModels(List models) { + return serviceImpl.updateUserModels(models); + } + @Override public boolean updateUserModel(String username, UserModel model) { if (supportsCredentialChanges()) { @@ -232,6 +237,7 @@ public class GitblitUserService implements IUserService { } @Override + @Deprecated public boolean setTeamnamesForRepositoryRole(String role, List teamnames) { return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames); } @@ -246,6 +252,11 @@ public class GitblitUserService implements IUserService { return serviceImpl.updateTeamModel(model); } + @Override + public boolean updateTeamModels(List models) { + return serviceImpl.updateTeamModels(models); + } + @Override public boolean updateTeamModel(String teamname, TeamModel model) { if (!supportsTeamMembershipChanges()) { @@ -275,6 +286,7 @@ public class GitblitUserService implements IUserService { } @Override + @Deprecated public boolean setUsernamesForRepositoryRole(String role, List usernames) { return serviceImpl.setUsernamesForRepositoryRole(role, usernames); } diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java index 8822d024..059d648a 100644 --- a/src/com/gitblit/IUserService.java +++ b/src/com/gitblit/IUserService.java @@ -126,6 +126,15 @@ public interface IUserService { */ boolean updateUserModel(UserModel model); + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + boolean updateUserModels(List models); + /** * Adds/updates a user object keyed by username. This method allows for * renaming a user. @@ -205,7 +214,8 @@ public interface IUserService { * @param teamnames * @return true if successful * @since 0.8.0 - */ + */ + @Deprecated boolean setTeamnamesForRepositoryRole(String role, List teamnames); /** @@ -226,6 +236,15 @@ public interface IUserService { */ boolean updateTeamModel(TeamModel model); + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + boolean updateTeamModels(List models); + /** * Updates/writes and replaces a complete team object keyed by teamname. * This method allows for renaming a team. @@ -277,6 +296,7 @@ public interface IUserService { * @param usernames * @return true if successful */ + @Deprecated boolean setUsernamesForRepositoryRole(String role, List usernames); /** diff --git a/src/com/gitblit/PagesFilter.java b/src/com/gitblit/PagesFilter.java index c092c64d..11cdfa56 100644 --- a/src/com/gitblit/PagesFilter.java +++ b/src/com/gitblit/PagesFilter.java @@ -111,6 +111,6 @@ public class PagesFilter extends AccessRestrictionFilter { */ @Override protected boolean canAccess(RepositoryModel repository, UserModel user, String action) { - return user.canAccessRepository(repository); + return user.canView(repository); } } diff --git a/src/com/gitblit/SyndicationFilter.java b/src/com/gitblit/SyndicationFilter.java index 0dff1c87..61bf2258 100644 --- a/src/com/gitblit/SyndicationFilter.java +++ b/src/com/gitblit/SyndicationFilter.java @@ -113,7 +113,7 @@ public class SyndicationFilter extends AuthenticationFilter { return; } else { // check user access for request - if (user.canAdmin || user.canAccessRepository(model)) { + if (user.canView(model)) { // authenticated request permitted. // pass processing to the restricted servlet. newSession(authenticatedRequest, httpResponse); diff --git a/src/com/gitblit/models/RepositoryModel.java b/src/com/gitblit/models/RepositoryModel.java index caf7e7e4..914523df 100644 --- a/src/com/gitblit/models/RepositoryModel.java +++ b/src/com/gitblit/models/RepositoryModel.java @@ -88,7 +88,8 @@ public class RepositoryModel implements Serializable, Comparable(); - this.federationStrategy = FederationStrategy.FEDERATE_THIS; + this.federationStrategy = FederationStrategy.FEDERATE_THIS; + this.projectPath = StringUtils.getFirstPathElement(name); } public List getLocalBranches() { @@ -175,8 +176,8 @@ public class RepositoryModel implements Serializable, Comparable { // field names are reflectively mapped in EditTeam page public String name; public final Set users = new HashSet(); + // retained for backwards-compatibility with RPC clients + @Deprecated public final Set repositories = new HashSet(); + public final Map permissions = new HashMap(); public final Set mailingLists = new HashSet(); public final List preReceiveScripts = new ArrayList(); public final List postReceiveScripts = new ArrayList(); @@ -45,24 +54,136 @@ public class TeamModel implements Serializable, Comparable { this.name = name; } + /** + * @use hasRepositoryPermission + * @param name + * @return + */ + @Deprecated + @Unused public boolean hasRepository(String name) { - return repositories.contains(name.toLowerCase()); + return hasRepositoryPermission(name); } + @Deprecated + @Unused public void addRepository(String name) { - repositories.add(name.toLowerCase()); + addRepositoryPermission(name); } + @Deprecated + @Unused public void addRepositories(Collection names) { - for (String name:names) { - repositories.add(name.toLowerCase()); - } - } + addRepositoryPermissions(names); + } + @Deprecated + @Unused public void removeRepository(String name) { - repositories.remove(name.toLowerCase()); + removeRepositoryPermission(name); + } + + /** + * Returns true if the team has any type of specified access permission for + * this repository. + * + * @param name + * @return true if team has a specified access permission for the repository + */ + public boolean hasRepositoryPermission(String name) { + String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); + return permissions.containsKey(repository) || repositories.contains(repository); } + /** + * Adds a repository permission to the team. + *

+ * Role may be formatted as: + *

    + *
  • myrepo.git (this is implicitly RW+) + *
  • RW+:myrepo.git + *
+ * @param role + */ + public void addRepositoryPermission(String role) { + AccessPermission permission = AccessPermission.permissionFromRole(role); + String repository = AccessPermission.repositoryFromRole(role).toLowerCase(); + repositories.add(repository); + permissions.put(repository, permission); + } + + public void addRepositoryPermissions(Collection roles) { + for (String role:roles) { + addRepositoryPermission(role); + } + } + + public AccessPermission removeRepositoryPermission(String name) { + String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); + repositories.remove(repository); + return permissions.remove(repository); + } + + public void setRepositoryPermission(String repository, AccessPermission permission) { + permissions.put(repository.toLowerCase(), permission); + repositories.add(repository.toLowerCase()); + } + + public AccessPermission getRepositoryPermission(RepositoryModel repository) { + AccessPermission permission = AccessPermission.NONE; + if (permissions.containsKey(repository.name.toLowerCase())) { + AccessPermission p = permissions.get(repository.name.toLowerCase()); + if (p != null) { + permission = p; + } + } + return permission; + } + + private boolean canAccess(RepositoryModel repository, AccessRestrictionType ifRestriction, AccessPermission requirePermission) { + if (repository.accessRestriction.atLeast(ifRestriction)) { + AccessPermission permission = getRepositoryPermission(repository); + return permission.atLeast(requirePermission); + } + return true; + } + + public boolean canView(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.VIEW, AccessPermission.VIEW); + } + + public boolean canClone(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.CLONE, AccessPermission.CLONE); + } + + public boolean canPush(RepositoryModel repository) { + if (repository.isFrozen) { + return false; + } + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.PUSH); + } + + public boolean canCreateRef(RepositoryModel repository) { + if (repository.isFrozen) { + return false; + } + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.CREATE); + } + + public boolean canDeleteRef(RepositoryModel repository) { + if (repository.isFrozen) { + return false; + } + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.DELETE); + } + + public boolean canRewindRef(RepositoryModel repository) { + if (repository.isFrozen) { + return false; + } + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.REWIND); + } + public boolean hasUser(String name) { return users.contains(name.toLowerCase()); } diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java index 94bd055d..ee730257 100644 --- a/src/com/gitblit/models/UserModel.java +++ b/src/com/gitblit/models/UserModel.java @@ -17,11 +17,15 @@ package com.gitblit.models; import java.io.Serializable; import java.security.Principal; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.Constants.AuthorizationControl; +import com.gitblit.Constants.Unused; import com.gitblit.utils.StringUtils; /** @@ -48,7 +52,10 @@ public class UserModel implements Principal, Serializable, Comparable public boolean canFork; public boolean canCreate; public boolean excludeFromFederation; + // retained for backwards-compatibility with RPC clients + @Deprecated public final Set repositories = new HashSet(); + public final Map permissions = new HashMap(); public final Set teams = new HashSet(); // non-persisted fields @@ -77,6 +84,8 @@ public class UserModel implements Principal, Serializable, Comparable || hasTeamAccess(repositoryName); } + @Deprecated + @Unused public boolean canAccessRepository(RepositoryModel repository) { boolean isOwner = !StringUtils.isEmpty(repository.owner) && repository.owner.equals(username); @@ -85,62 +94,170 @@ public class UserModel implements Principal, Serializable, Comparable || hasTeamAccess(repository.name) || allowAuthenticated; } + @Deprecated + @Unused public boolean hasTeamAccess(String repositoryName) { for (TeamModel team : teams) { - if (team.hasRepository(repositoryName)) { + if (team.hasRepositoryPermission(repositoryName)) { return true; } } return false; } - public boolean canViewRepository(RepositoryModel repository) { - if (canAdmin) { - return true; + @Deprecated + @Unused + public boolean hasRepository(String name) { + return hasRepositoryPermission(name); + } + + @Deprecated + @Unused + public void addRepository(String name) { + addRepositoryPermission(name); + } + + @Deprecated + @Unused + public void removeRepository(String name) { + removeRepositoryPermission(name); + } + + /** + * Returns true if the user has any type of specified access permission for + * this repository. + * + * @param name + * @return true if user has a specified access permission for the repository + */ + public boolean hasRepositoryPermission(String name) { + String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); + return permissions.containsKey(repository) || repositories.contains(repository); + } + + /** + * Adds a repository permission to the team. + *

+ * Role may be formatted as: + *

    + *
  • myrepo.git (this is implicitly RW+) + *
  • RW+:myrepo.git + *
+ * @param role + */ + public void addRepositoryPermission(String role) { + AccessPermission permission = AccessPermission.permissionFromRole(role); + String repository = AccessPermission.repositoryFromRole(role).toLowerCase(); + repositories.add(repository); + permissions.put(repository, permission); + } + + public AccessPermission removeRepositoryPermission(String name) { + String repository = AccessPermission.repositoryFromRole(name).toLowerCase(); + repositories.remove(repository); + return permissions.remove(repository); + } + + public void setRepositoryPermission(String repository, AccessPermission permission) { + permissions.put(repository.toLowerCase(), permission); + } + + public AccessPermission getRepositoryPermission(RepositoryModel repository) { + if (canAdmin || repository.isOwner(username) || repository.isUsersPersonalRepository(username)) { + return AccessPermission.REWIND; + } + if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl) && isAuthenticated) { + // AUTHENTICATED is a shortcut for authorizing all logged-in users RW access + return AccessPermission.REWIND; } - if (repository.accessRestriction.atLeast(AccessRestrictionType.VIEW)) { - return canAccessRepository(repository); + + // determine best permission available based on user's personal permissions + // and the permissions of teams of which the user belongs + AccessPermission permission = AccessPermission.NONE; + if (permissions.containsKey(repository.name.toLowerCase())) { + AccessPermission p = permissions.get(repository.name.toLowerCase()); + if (p != null) { + permission = p; + } + } + + for (TeamModel team : teams) { + AccessPermission p = team.getRepositoryPermission(repository); + if (permission == null || p.exceeds(permission)) { + // use team permission + permission = p; + } + } + return permission; + } + + private boolean canAccess(RepositoryModel repository, AccessRestrictionType ifRestriction, AccessPermission requirePermission) { + if (repository.accessRestriction.atLeast(ifRestriction)) { + AccessPermission permission = getRepositoryPermission(repository); + return permission.atLeast(requirePermission); } return true; } - public boolean canForkRepository(RepositoryModel repository) { - if (canAdmin) { - return true; + public boolean canView(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.VIEW, AccessPermission.VIEW); + } + + public boolean canClone(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.CLONE, AccessPermission.CLONE); + } + + public boolean canPush(RepositoryModel repository) { + if (repository.isFrozen) { + return false; + } + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.PUSH); + } + + public boolean canCreateRef(RepositoryModel repository) { + if (repository.isFrozen) { + return false; } - if (!canFork) { - // user has been prohibited from forking + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.CREATE); + } + + public boolean canDeleteRef(RepositoryModel repository) { + if (repository.isFrozen) { return false; } - if (!isAuthenticated) { - // unauthenticated user model + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.DELETE); + } + + public boolean canRewindRef(RepositoryModel repository) { + if (repository.isFrozen) { return false; } - if (("~" + username).equalsIgnoreCase(repository.projectPath)) { - // this repository is already a personal repository + return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.REWIND); + } + + public boolean canFork(RepositoryModel repository) { + if (repository.isUsersPersonalRepository(username)) { + // can not fork your own repository return false; } + if (canAdmin || repository.isOwner(username)) { + return true; + } if (!repository.allowForks) { - // repository prohibits forks return false; } - if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) { - return canAccessRepository(repository); + if (!isAuthenticated || !canFork) { + return false; } - // repository is not clone-restricted - return true; - } - - public boolean hasRepository(String name) { - return repositories.contains(name.toLowerCase()); + return canClone(repository); } - - public void addRepository(String name) { - repositories.add(name.toLowerCase()); + + public boolean canDelete(RepositoryModel model) { + return canAdmin || model.isUsersPersonalRepository(username); } - - public void removeRepository(String name) { - repositories.remove(name.toLowerCase()); + + public boolean canEdit(RepositoryModel model) { + return canAdmin || model.isUsersPersonalRepository(username) || model.isOwner(username); } public boolean isTeamMember(String teamname) { diff --git a/src/com/gitblit/utils/JsonUtils.java b/src/com/gitblit/utils/JsonUtils.java index bc9a1e00..24f4ecb8 100644 --- a/src/com/gitblit/utils/JsonUtils.java +++ b/src/com/gitblit/utils/JsonUtils.java @@ -32,6 +32,7 @@ import java.util.Locale; import java.util.Map; import java.util.TimeZone; +import com.gitblit.Constants.AccessPermission; import com.gitblit.GitBlitException.ForbiddenException; import com.gitblit.GitBlitException.NotAllowedException; import com.gitblit.GitBlitException.UnauthorizedException; @@ -266,6 +267,7 @@ public class JsonUtils { public static Gson gson(ExclusionStrategy... strategies) { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter()); + builder.registerTypeAdapter(AccessPermission.class, new AccessPermissionTypeAdapter()); builder.setPrettyPrinting(); if (!ArrayUtils.isEmpty(strategies)) { builder.setExclusionStrategies(strategies); @@ -303,6 +305,24 @@ public class JsonUtils { } } } + + private static class AccessPermissionTypeAdapter implements JsonSerializer, JsonDeserializer { + + private AccessPermissionTypeAdapter() { + } + + @Override + public synchronized JsonElement serialize(AccessPermission permission, Type type, + JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(permission.code); + } + + @Override + public synchronized AccessPermission deserialize(JsonElement jsonElement, Type type, + JsonDeserializationContext jsonDeserializationContext) { + return AccessPermission.fromCode(jsonElement.getAsString()); + } + } public static class ExcludeField implements ExclusionStrategy { diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java index 00d9677f..cce323fd 100644 --- a/src/com/gitblit/wicket/pages/BasePage.java +++ b/src/com/gitblit/wicket/pages/BasePage.java @@ -297,7 +297,7 @@ public abstract class BasePage extends WebPage { for (ProjectModel projectModel : availableModels) { for (String repositoryName : projectModel.repositories) { for (TeamModel teamModel : teamModels) { - if (teamModel.hasRepository(repositoryName)) { + if (teamModel.hasRepositoryPermission(repositoryName)) { models.add(projectModel); } } diff --git a/src/com/gitblit/wicket/pages/ForkPage.java b/src/com/gitblit/wicket/pages/ForkPage.java index 082dab51..340bd823 100644 --- a/src/com/gitblit/wicket/pages/ForkPage.java +++ b/src/com/gitblit/wicket/pages/ForkPage.java @@ -40,7 +40,7 @@ public class ForkPage extends RepositoryPage { RepositoryModel repository = getRepositoryModel(); UserModel user = session.getUser(); - boolean canFork = user.canForkRepository(repository); + boolean canFork = user.canFork(repository); if (!canFork) { // redirect to the summary page if this repository is not empty diff --git a/src/com/gitblit/wicket/pages/ForksPage.java b/src/com/gitblit/wicket/pages/ForksPage.java index 2e67e2b7..6155f3ed 100644 --- a/src/com/gitblit/wicket/pages/ForksPage.java +++ b/src/com/gitblit/wicket/pages/ForksPage.java @@ -94,7 +94,7 @@ public class ForksPage extends RepositoryPage { if (user == null) { user = UserModel.ANONYMOUS; } - if (user.canViewRepository(repository)) { + if (user.canView(repository)) { if (pageRepository.equals(repository)) { // do not link to self item.add(new Label("aFork", StringUtils.stripDotGit(repo))); diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java index 2afc2c4d..9048eba3 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/com/gitblit/wicket/pages/RepositoryPage.java @@ -209,7 +209,7 @@ public abstract class RepositoryPage extends BasePage { if (origin == null) { // no origin repository add(new Label("originRepository").setVisible(false)); - } else if (!user.canViewRepository(origin)) { + } else if (!user.canView(origin)) { // show origin repository without link Fragment forkFrag = new Fragment("originRepository", "originFragment", this); forkFrag.add(new Label("originRepository", StringUtils.stripDotGit(model.originRepository))); @@ -242,7 +242,7 @@ public abstract class RepositoryPage extends BasePage { } else { String fork = GitBlit.self().getFork(user.username, model.name); boolean hasFork = fork != null; - boolean canFork = user.canForkRepository(model); + boolean canFork = user.canFork(model); if (hasFork || !canFork) { // user not allowed to fork or fork already exists or repo forbids forking diff --git a/src/com/gitblit/wicket/pages/RootPage.java b/src/com/gitblit/wicket/pages/RootPage.java index 1e6f130c..adcd7b16 100644 --- a/src/com/gitblit/wicket/pages/RootPage.java +++ b/src/com/gitblit/wicket/pages/RootPage.java @@ -418,7 +418,7 @@ public abstract class RootPage extends BasePage { // brute-force our way through finding the matching models for (RepositoryModel repositoryModel : availableModels) { for (TeamModel teamModel : teamModels) { - if (teamModel.hasRepository(repositoryModel.name)) { + if (teamModel.hasRepositoryPermission(repositoryModel.name)) { models.add(repositoryModel); } } diff --git a/tests/com/gitblit/tests/FederationTests.java b/tests/com/gitblit/tests/FederationTests.java index 2c4ffdc9..c8f686ad 100644 --- a/tests/com/gitblit/tests/FederationTests.java +++ b/tests/com/gitblit/tests/FederationTests.java @@ -136,7 +136,7 @@ public class FederationTests { TeamModel team = new TeamModel("testteam"); team.addUser("test"); - team.addRepository("helloworld.git"); + team.addRepositoryPermission("helloworld.git"); assertTrue(RpcUtils.createTeam(team, url, account, password.toCharArray())); users = FederationUtils.getUsers(getRegistration()); diff --git a/tests/com/gitblit/tests/GitBlitSuite.java b/tests/com/gitblit/tests/GitBlitSuite.java index 07c5e08e..22bcf137 100644 --- a/tests/com/gitblit/tests/GitBlitSuite.java +++ b/tests/com/gitblit/tests/GitBlitSuite.java @@ -49,7 +49,7 @@ import com.gitblit.utils.JGitUtils; @RunWith(Suite.class) @SuiteClasses({ ArrayUtilsTest.class, FileUtilsTest.class, TimeUtilsTest.class, StringUtilsTest.class, Base64Test.class, JsonUtilsTest.class, ByteFormatTest.class, - ObjectCacheTest.class, UserServiceTest.class, LdapUserServiceTest.class, + ObjectCacheTest.class, PermissionsTest.class, UserServiceTest.class, LdapUserServiceTest.class, MarkdownUtilsTest.class, JGitUtilsTest.class, SyndicationUtilsTest.class, DiffUtilsTest.class, MetricUtilsTest.class, TicgitUtilsTest.class, GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, diff --git a/tests/com/gitblit/tests/GitBlitTest.java b/tests/com/gitblit/tests/GitBlitTest.java index 418f9384..a188f180 100644 --- a/tests/com/gitblit/tests/GitBlitTest.java +++ b/tests/com/gitblit/tests/GitBlitTest.java @@ -52,20 +52,21 @@ public class GitBlitTest { List users = GitBlit.self().getAllUsernames(); assertTrue("No users found!", users.size() > 0); assertTrue("Admin not found", users.contains("admin")); - UserModel model = GitBlit.self().getUserModel("admin"); - assertEquals("admin", model.toString()); - assertTrue("Admin missing #admin role!", model.canAdmin); - model.canAdmin = false; - assertFalse("Admin should not have #admin!", model.canAdmin); + UserModel user = GitBlit.self().getUserModel("admin"); + assertEquals("admin", user.toString()); + assertTrue("Admin missing #admin role!", user.canAdmin); + user.canAdmin = false; + assertFalse("Admin should not have #admin!", user.canAdmin); String repository = GitBlitSuite.getHelloworldRepository().getDirectory().getName(); RepositoryModel repositoryModel = GitBlit.self().getRepositoryModel(repository); + repositoryModel.accessRestriction = AccessRestrictionType.VIEW; assertFalse("Admin can still access repository!", - model.canAccessRepository(repositoryModel)); - model.addRepository(repository); - assertTrue("Admin can't access repository!", model.canAccessRepository(repositoryModel)); - assertEquals(GitBlit.self().getRepositoryModel(model, "pretend"), null); - assertNotNull(GitBlit.self().getRepositoryModel(model, repository)); - assertTrue(GitBlit.self().getRepositoryModels(model).size() > 0); + user.canView(repositoryModel)); + user.addRepositoryPermission(repository); + assertTrue("Admin can't access repository!", user.canView(repositoryModel)); + assertEquals(GitBlit.self().getRepositoryModel(user, "pretend"), null); + assertNotNull(GitBlit.self().getRepositoryModel(user, repository)); + assertTrue(GitBlit.self().getRepositoryModels(user).size() > 0); } @Test diff --git a/tests/com/gitblit/tests/GitServletTest.java b/tests/com/gitblit/tests/GitServletTest.java index bdbb2a5a..09e0e5ad 100644 --- a/tests/com/gitblit/tests/GitServletTest.java +++ b/tests/com/gitblit/tests/GitServletTest.java @@ -13,18 +13,28 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.util.FileUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.Constants.AuthorizationControl; import com.gitblit.GitBlit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; +import com.gitblit.utils.JGitUtils; public class GitServletTest { @@ -233,6 +243,213 @@ public class GitServletTest { } close(git); } + + @Test + public void testBlockClone() throws Exception { + testRefChange(AccessPermission.VIEW, null, null, null); + } + + @Test + public void testBlockPush() throws Exception { + testRefChange(AccessPermission.CLONE, null, null, null); + } + + @Test + public void testBlockBranchCreation() throws Exception { + testRefChange(AccessPermission.PUSH, Status.REJECTED_OTHER_REASON, null, null); + } + + @Test + public void testBlockBranchDeletion() throws Exception { + testRefChange(AccessPermission.CREATE, Status.OK, Status.REJECTED_OTHER_REASON, null); + } + + @Test + public void testBlockBranchRewind() throws Exception { + testRefChange(AccessPermission.DELETE, Status.OK, Status.OK, Status.REJECTED_OTHER_REASON); + } + + @Test + public void testBranchRewind() throws Exception { + testRefChange(AccessPermission.REWIND, Status.OK, Status.OK, Status.OK); + } + + private void testRefChange(AccessPermission permission, Status expectedCreate, Status expectedDelete, Status expectedRewind) throws Exception { + + UserModel user = new UserModel("james"); + user.password = "james"; + + if (GitBlit.self().getUserModel(user.username) != null) { + GitBlit.self().deleteUser(user.username); + } + + CredentialsProvider cp = new UsernamePasswordCredentialsProvider(user.username, user.password); + + // fork from original to a temporary bare repo + File refChecks = new File(GitBlitSuite.REPOSITORIES, "refchecks/ticgit.git"); + if (refChecks.exists()) { + FileUtils.delete(refChecks, FileUtils.RECURSIVE); + } + CloneCommand clone = Git.cloneRepository(); + clone.setURI(MessageFormat.format("{0}/git/ticgit.git", url)); + clone.setDirectory(refChecks); + clone.setBare(true); + clone.setCloneAllBranches(true); + clone.setCredentialsProvider(cp); + close(clone.call()); + + // elevate repository to clone permission + RepositoryModel model = GitBlit.self().getRepositoryModel("refchecks/ticgit.git"); + switch (permission) { + case VIEW: + model.accessRestriction = AccessRestrictionType.CLONE; + break; + case CLONE: + model.accessRestriction = AccessRestrictionType.CLONE; + break; + default: + model.accessRestriction = AccessRestrictionType.PUSH; + } + model.authorizationControl = AuthorizationControl.NAMED; + + // grant user specified + user.setRepositoryPermission(model.name, permission); + + GitBlit.self().updateUserModel(user.username, user, true); + GitBlit.self().updateRepositoryModel(model.name, model, false); + + // clone temp bare repo to working copy + File local = new File(GitBlitSuite.REPOSITORIES, "refchecks/ticgit-wc"); + if (local.exists()) { + FileUtils.delete(local, FileUtils.RECURSIVE); + } + clone = Git.cloneRepository(); + clone.setURI(MessageFormat.format("{0}/git/{1}", url, model.name)); + clone.setDirectory(local); + clone.setBare(false); + clone.setCloneAllBranches(true); + clone.setCredentialsProvider(cp); + + try { + close(clone.call()); + } catch (GitAPIException e) { + if (permission.atLeast(AccessPermission.CLONE)) { + throw e; + } else { + // user does not have clone permission + assertTrue(e.getMessage(), e.getMessage().contains("not permitted")); + return; + } + } + + Git git = Git.open(local); + + // commit a file and push it + File file = new File(local, "PUSHCHK"); + OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET); + BufferedWriter w = new BufferedWriter(os); + w.write("// " + new Date().toString() + "\n"); + w.close(); + git.add().addFilepattern(file.getName()).call(); + git.commit().setMessage("push test").call(); + Iterable results = null; + try { + results = git.push().setCredentialsProvider(cp).setRemote("origin").call(); + } catch (GitAPIException e) { + if (permission.atLeast(AccessPermission.PUSH)) { + throw e; + } else { + // user does not have push permission + assertTrue(e.getMessage(), e.getMessage().contains("not permitted")); + close(git); + return; + } + } + + for (PushResult result : results) { + RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master"); + Status status = ref.getStatus(); + if (permission.atLeast(AccessPermission.PUSH)) { + assertTrue("User failed to push commit?! " + status.name(), Status.OK.equals(status)); + } else { + assertTrue("User was able to push commit! " + status.name(), Status.REJECTED_OTHER_REASON.equals(status)); + close(git); + // skip delete test + return; + } + } + + // create a local branch and push the new branch back to the origin + git.branchCreate().setName("protectme").call(); + RefSpec refSpec = new RefSpec("refs/heads/protectme:refs/heads/protectme"); + results = git.push().setCredentialsProvider(cp).setRefSpecs(refSpec).setRemote("origin").call(); + for (PushResult result : results) { + RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/protectme"); + Status status = ref.getStatus(); + if (Status.OK.equals(expectedCreate)) { + assertTrue("User failed to push creation?! " + status.name(), status.equals(expectedCreate)); + } else { + assertTrue("User was able to push ref creation! " + status.name(), status.equals(expectedCreate)); + close(git); + // skip delete test + return; + } + } + + // delete the branch locally + git.branchDelete().setBranchNames("protectme").call(); + + // push a delete ref command + refSpec = new RefSpec(":refs/heads/protectme"); + results = git.push().setCredentialsProvider(cp).setRefSpecs(refSpec).setRemote("origin").call(); + for (PushResult result : results) { + RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/protectme"); + Status status = ref.getStatus(); + if (Status.OK.equals(expectedDelete)) { + assertTrue("User failed to push ref deletion?! " + status.name(), status.equals(Status.OK)); + } else { + assertTrue("User was able to push ref deletion?! " + status.name(), status.equals(expectedDelete)); + close(git); + // skip rewind test + return; + } + } + + // rewind master by two commits + git.reset().setRef("HEAD~2").setMode(ResetType.HARD).call(); + + // commit a change on this detached HEAD + file = new File(local, "REWINDCHK"); + os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET); + w = new BufferedWriter(os); + w.write("// " + new Date().toString() + "\n"); + w.close(); + git.add().addFilepattern(file.getName()).call(); + RevCommit commit = git.commit().setMessage("rewind master and new commit").call(); + + // Reset master to our new commit now we our local branch tip is no longer + // upstream of the remote branch tip. It is an alternate tip of the branch. + JGitUtils.setBranchRef(git.getRepository(), "refs/heads/master", commit.getName()); + + // Try pushing our new tip to the origin. + // This requires the server to "rewind" it's master branch and update it + // to point to our alternate tip. This leaves the original master tip + // unreferenced. + results = git.push().setCredentialsProvider(cp).setRemote("origin").setForce(true).call(); + for (PushResult result : results) { + RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master"); + Status status = ref.getStatus(); + if (Status.OK.equals(expectedRewind)) { + assertTrue("User failed to rewind master?! " + status.name(), status.equals(expectedRewind)); + } else { + assertTrue("User was able to rewind master?! " + status.name(), status.equals(expectedRewind)); + } + } + close(git); + + GitBlit.self().deleteUser(user.username); + } + private void close(Git git) { // really close the repository diff --git a/tests/com/gitblit/tests/JGitUtilsTest.java b/tests/com/gitblit/tests/JGitUtilsTest.java index f4870461..7e4d6309 100644 --- a/tests/com/gitblit/tests/JGitUtilsTest.java +++ b/tests/com/gitblit/tests/JGitUtilsTest.java @@ -35,6 +35,7 @@ import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; @@ -141,7 +142,8 @@ public class JGitUtilsTest { assertEquals(folder.lastModified(), JGitUtils.getLastChange(repository).getTime()); assertNull(JGitUtils.getCommit(repository, null)); repository.close(); - assertTrue(GitBlit.self().deleteRepository(repositoryName)); + RepositoryCache.close(repository); + FileUtils.delete(repository.getDirectory(), FileUtils.RECURSIVE); } } diff --git a/tests/com/gitblit/tests/PermissionsTest.java b/tests/com/gitblit/tests/PermissionsTest.java new file mode 100644 index 00000000..cb9925e8 --- /dev/null +++ b/tests/com/gitblit/tests/PermissionsTest.java @@ -0,0 +1,2391 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.tests; + +import java.util.Date; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.Constants.AccessRestrictionType; +import com.gitblit.Constants.AuthorizationControl; +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; + +/** + * Comprehensive, brute-force test of all permutations of discrete permissions. + * + * @author James Moger + * + */ +public class PermissionsTest extends Assert { + + /** + * Admin access rights/permissions + */ + @Test + public void testAdmin() throws Exception { + UserModel user = new UserModel("admin"); + user.canAdmin = true; + + for (AccessRestrictionType ar : AccessRestrictionType.values()) { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = ar; + + assertTrue("admin CAN NOT view!", user.canView(repository)); + assertTrue("admin CAN NOT clone!", user.canClone(repository)); + assertTrue("admin CAN NOT push!", user.canPush(repository)); + + assertTrue("admin CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("admin CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("admin CAN NOT rewind ref!", user.canRewindRef(repository)); + + assertTrue("admin CAN NOT fork!", user.canFork(repository)); + + assertTrue("admin CAN NOT delete!", user.canDelete(repository)); + assertTrue("admin CAN NOT edit!", user.canEdit(repository)); + } + } + + /** + * Anonymous access rights/permissions + */ + @Test + public void testAnonymous_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = UserModel.ANONYMOUS; + + // all permissions, except fork + assertTrue("anonymous CAN NOT view!", user.canView(repository)); + assertTrue("anonymous CAN NOT clone!", user.canClone(repository)); + assertTrue("anonymous CAN NOT push!", user.canPush(repository)); + + assertTrue("anonymous CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("anonymous CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("anonymous CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + + assertFalse("anonymous CAN delete!", user.canDelete(repository)); + assertFalse("anonymous CAN edit!", user.canEdit(repository)); + } + + @Test + public void testAnonymous_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = UserModel.ANONYMOUS; + + assertTrue("anonymous CAN NOT view!", user.canView(repository)); + assertTrue("anonymous CAN NOT clone!", user.canClone(repository)); + assertFalse("anonymous CAN push!", user.canPush(repository)); + + assertFalse("anonymous CAN create ref!", user.canCreateRef(repository)); + assertFalse("anonymous CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("anonymous CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + } + + @Test + public void testAnonymous_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = UserModel.ANONYMOUS; + + assertTrue("anonymous CAN NOT view!", user.canView(repository)); + assertFalse("anonymous CAN clone!", user.canClone(repository)); + assertFalse("anonymous CAN push!", user.canPush(repository)); + + assertFalse("anonymous CAN create ref!", user.canCreateRef(repository)); + assertFalse("anonymous CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("anonymous CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + } + + @Test + public void testAnonymous_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = UserModel.ANONYMOUS; + + assertFalse("anonymous CAN view!", user.canView(repository)); + assertFalse("anonymous CAN clone!", user.canClone(repository)); + assertFalse("anonymous CAN push!", user.canPush(repository)); + + assertFalse("anonymous CAN create ref!", user.canCreateRef(repository)); + assertFalse("anonymous CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("anonymous CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("anonymous CAN fork!", user.canFork(repository)); + } + + /** + * Authenticated access rights/permissions + */ + @Test + public void testAuthenticated_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.AUTHENTICATED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + + // all permissions, except fork + assertTrue("authenticated CAN NOT view!", user.canView(repository)); + assertTrue("authenticated CAN NOT clone!", user.canClone(repository)); + assertTrue("authenticated CAN NOT push!", user.canPush(repository)); + + assertTrue("authenticated CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("authenticated CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("authenticated CAN NOT rewind ref!", user.canRewindRef(repository)); + + user.canFork = false; + repository.allowForks = false; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + user.canFork = true; + assertTrue("authenticated CAN NOT fork!", user.canFork(repository)); + + assertFalse("authenticated CAN delete!", user.canDelete(repository)); + assertFalse("authenticated CAN edit!", user.canEdit(repository)); + } + + @Test + public void testAuthenticated_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.AUTHENTICATED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + + assertTrue("authenticated CAN NOT view!", user.canView(repository)); + assertTrue("authenticated CAN NOT clone!", user.canClone(repository)); + assertTrue("authenticated CAN NOT push!", user.canPush(repository)); + + assertTrue("authenticated CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("authenticated CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("authenticated CAN NOT rewind ref!", user.canRewindRef(repository)); + + user.canFork = false; + repository.allowForks = false; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + user.canFork = true; + assertTrue("authenticated CAN NOT fork!", user.canFork(repository)); + } + + @Test + public void testAuthenticated_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.AUTHENTICATED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + + assertTrue("authenticated CAN NOT view!", user.canView(repository)); + assertTrue("authenticated CAN NOT clone!", user.canClone(repository)); + assertTrue("authenticated CAN NOT push!", user.canPush(repository)); + + assertTrue("authenticated CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("authenticated CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("authenticated CAN NOT rewind ref!", user.canRewindRef(repository)); + + user.canFork = false; + repository.allowForks = false; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + user.canFork = true; + assertTrue("authenticated CAN NOT fork!", user.canFork(repository)); + } + + @Test + public void testAuthenticated_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.AUTHENTICATED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + + assertTrue("authenticated CAN NOT view!", user.canView(repository)); + assertTrue("authenticated CAN NOT clone!", user.canClone(repository)); + assertTrue("authenticated CAN NOT push!", user.canPush(repository)); + + assertTrue("authenticated CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("authenticated CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("authenticated CAN NOT rewind ref!", user.canRewindRef(repository)); + + user.canFork = false; + repository.allowForks = false; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("authenticated CAN fork!", user.canFork(repository)); + user.canFork = true; + assertTrue("authenticated CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_NONE = NO access restriction, NO access permission + */ + @Test + public void testNamed_NONE_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + + assertFalse("named CAN delete!", user.canDelete(repository)); + assertFalse("named CAN edit!", user.canEdit(repository)); + } + + /** + * PUSH_NONE = PUSH access restriction, NO access permission + */ + @Test + public void testNamed_PUSH_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_NONE = CLONE access restriction, NO access permission + */ + @Test + public void testNamed_CLONE_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertFalse("named CAN clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_NONE = VIEW access restriction, NO access permission + */ + @Test + public void testNamed_VIEW_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + + assertFalse("named CAN view!", user.canView(repository)); + assertFalse("named CAN clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("named CAN NOT fork!", user.canFork(repository)); + } + + + /** + * NONE_VIEW = NO access restriction, VIEW access permission. + * (not useful scenario) + */ + @Test + public void testNamed_NONE_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * PUSH_VIEW = PUSH access restriction, VIEW access permission + */ + @Test + public void testNamed_PUSH_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_VIEW = CLONE access restriction, VIEW access permission + */ + @Test + public void testNamed_CLONE_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertFalse("named CAN clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_VIEW = VIEW access restriction, VIEW access permission + */ + @Test + public void testNamed_VIEW_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertFalse("named CAN clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertFalse("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_CLONE = NO access restriction, CLONE access permission. + * (not useful scenario) + */ + @Test + public void testNamed_NONE_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * PUSH_CLONE = PUSH access restriction, CLONE access permission + */ + @Test + public void testNamed_PUSH_READ() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_CLONE = CLONE access restriction, CLONE access permission + */ + @Test + public void testNamed_CLONE_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_CLONE = VIEW access restriction, CLONE access permission + */ + @Test + public void testNamed_VIEW_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertFalse("named CAN push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_PUSH = NO access restriction, PUSH access permission. + * (not useful scenario) + */ + @Test + public void testNamed_NONE_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * PUSH_PUSH = PUSH access restriction, PUSH access permission + */ + @Test + public void testNamed_PUSH_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_PUSH = CLONE access restriction, PUSH access permission + */ + @Test + public void testNamed_CLONE_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete red!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_PUSH = VIEW access restriction, PUSH access permission + */ + @Test + public void testNamed_VIEW_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN not push!", user.canPush(repository)); + + assertFalse("named CAN create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_CREATE = NO access restriction, CREATE access permission. + * (not useful scenario) + */ + @Test + public void testNamed_NONE_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * PUSH_CREATE = PUSH access restriction, CREATE access permission + */ + @Test + public void testNamed_PUSH_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_CREATE = CLONE access restriction, CREATE access permission + */ + @Test + public void testNamed_CLONE_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete red!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_CREATE = VIEW access restriction, CREATE access permission + */ + @Test + public void testNamed_VIEW_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN not push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertFalse("named CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_DELETE = NO access restriction, DELETE access permission. + * (not useful scenario) + */ + @Test + public void testNamed_NONE_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * PUSH_DELETE = PUSH access restriction, DELETE access permission + */ + @Test + public void testNamed_PUSH_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_DELETE = CLONE access restriction, DELETE access permission + */ + @Test + public void testNamed_CLONE_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete red!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_DELETE = VIEW access restriction, DELETE access permission + */ + @Test + public void testNamed_VIEW_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN not push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertFalse("named CAN rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_REWIND = NO access restriction, REWIND access permission. + * (not useful scenario) + */ + @Test + public void testNamed_NONE_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * PUSH_REWIND = PUSH access restriction, REWIND access permission + */ + @Test + public void testNamed_PUSH_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * CLONE_REWIND = CLONE access restriction, REWIND access permission + */ + @Test + public void testNamed_CLONE_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * VIEW_REWIND = VIEW access restriction, REWIND access permission + */ + @Test + public void testNamed_VIEW_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + user.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("named CAN NOT view!", user.canView(repository)); + assertTrue("named CAN NOT clone!", user.canClone(repository)); + assertTrue("named CAN NOT push!", user.canPush(repository)); + + assertTrue("named CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("named CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("named CAN NOT rewind ref!", user.canRewindRef(repository)); + + repository.allowForks = false; + user.canFork = false; + assertFalse("named CAN fork!", user.canFork(repository)); + user.canFork = true; + assertFalse("named CAN fork!", user.canFork(repository)); + repository.allowForks = true; + assertTrue("named CAN NOT fork!", user.canFork(repository)); + } + + /** + * NONE_NONE = NO access restriction, NO access permission + */ + @Test + public void testTeam_NONE_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_NONE = PUSH access restriction, NO access permission + */ + @Test + public void testTeam_PUSH_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_NONE = CLONE access restriction, NO access permission + */ + @Test + public void testTeam_CLONE_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertFalse("team CAN clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_NONE = VIEW access restriction, NO access permission + */ + @Test + public void testTeam_VIEW_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + + assertFalse("team CAN view!", team.canView(repository)); + assertFalse("team CAN clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_PUSH = NO access restriction, PUSH access permission + * (not useful scenario) + */ + @Test + public void testTeam_NONE_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_PUSH = PUSH access restriction, PUSH access permission + */ + @Test + public void testTeam_PUSH_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_PUSH = CLONE access restriction, PUSH access permission + */ + @Test + public void testTeam_CLONE_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_PUSH = VIEW access restriction, PUSH access permission + */ + @Test + public void testTeam_VIEW_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_CREATE = NO access restriction, CREATE access permission + * (not useful scenario) + */ + @Test + public void testTeam_NONE_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_CREATE = PUSH access restriction, CREATE access permission + */ + @Test + public void testTeam_PUSH_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_CREATE = CLONE access restriction, CREATE access permission + */ + @Test + public void testTeam_CLONE_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_CREATE = VIEW access restriction, CREATE access permission + */ + @Test + public void testTeam_VIEW_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_DELETE = NO access restriction, DELETE access permission + * (not useful scenario) + */ + @Test + public void testTeam_NONE_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_DELETE = PUSH access restriction, DELETE access permission + */ + @Test + public void testTeam_PUSH_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_DELETE = CLONE access restriction, DELETE access permission + */ + @Test + public void testTeam_CLONE_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_DELETE = VIEW access restriction, DELETE access permission + */ + @Test + public void testTeam_VIEW_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_REWIND = NO access restriction, REWIND access permission + * (not useful scenario) + */ + @Test + public void testTeam_NONE_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_REWIND = PUSH access restriction, REWIND access permission + */ + @Test + public void testTeam_PUSH_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_REWIND = CLONE access restriction, REWIND access permission + */ + @Test + public void testTeam_CLONE_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_REWIND = VIEW access restriction, REWIND access permission + */ + @Test + public void testTeam_VIEW_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_CLONE = NO access restriction, CLONE access permission + * (not useful scenario) + */ + @Test + public void testTeam_NONE_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_CLONE = PUSH access restriction, CLONE access permission + */ + @Test + public void testTeam_PUSH_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_CLONE = CLONE access restriction, CLONE access permission + */ + @Test + public void testTeam_CLONE_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_CLONE = VIEW access restriction, CLONE access permission + */ + @Test + public void testTeam_VIEW_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_VIEW = NO access restriction, VIEW access permission + * (not useful scenario) + */ + @Test + public void testTeam_NONE_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertTrue("team CAN NOT push!", team.canPush(repository)); + + assertTrue("team CAN NOT create ref!", team.canCreateRef(repository)); + assertTrue("team CAN NOT delete ref!", team.canDeleteRef(repository)); + assertTrue("team CAN NOT rewind ref!", team.canRewindRef(repository)); + } + + /** + * PUSH_VIEW = PUSH access restriction, VIEW access permission + */ + @Test + public void testTeam_PUSH_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertTrue("team CAN NOT clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * CLONE_VIEW = CLONE access restriction, VIEW access permission + */ + @Test + public void testTeam_CLONE_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertFalse("team CAN clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * VIEW_VIEW = VIEW access restriction, VIEW access permission + */ + @Test + public void testTeam_VIEW_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + + assertTrue("team CAN NOT view!", team.canView(repository)); + assertFalse("team CAN clone!", team.canClone(repository)); + assertFalse("team CAN push!", team.canPush(repository)); + + assertFalse("team CAN create ref!", team.canCreateRef(repository)); + assertFalse("team CAN delete ref!", team.canDeleteRef(repository)); + assertFalse("team CAN rewind ref!", team.canRewindRef(repository)); + } + + /** + * NONE_NONE = NO access restriction, NO access permission + */ + @Test + public void testTeamMember_NONE_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_NONE = PUSH access restriction, NO access permission + */ + @Test + public void testTeamMember_PUSH_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_NONE = CLONE access restriction, NO access permission + */ + @Test + public void testTeamMember_CLONE_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertFalse("team member CAN clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_NONE = VIEW access restriction, NO access permission + */ + @Test + public void testTeamMember_VIEW_NONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertFalse("team member CAN view!", user.canView(repository)); + assertFalse("team member CAN clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * NONE_PUSH = NO access restriction, PUSH access permission + * (not useful scenario) + */ + @Test + public void testTeamMember_NONE_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_PUSH = PUSH access restriction, PUSH access permission + */ + @Test + public void testTeamMember_PUSH_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_PUSH = CLONE access restriction, PUSH access permission + */ + @Test + public void testTeamMember_CLONE_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_PUSH = VIEW access restriction, PUSH access permission + */ + @Test + public void testTeamMember_VIEW_PUSH() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.PUSH); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * NONE_CREATE = NO access restriction, CREATE access permission + * (not useful scenario) + */ + @Test + public void testTeamMember_NONE_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_CREATE = PUSH access restriction, CREATE access permission + */ + @Test + public void testTeamMember_PUSH_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_CREATE = CLONE access restriction, CREATE access permission + */ + @Test + public void testTeamMember_CLONE_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_CREATE = VIEW access restriction, CREATE access permission + */ + @Test + public void testTeamMember_VIEW_CREATE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CREATE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * NONE_DELETE = NO access restriction, DELETE access permission + * (not useful scenario) + */ + @Test + public void testTeamMember_NONE_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_DELETE = PUSH access restriction, DELETE access permission + */ + @Test + public void testTeamMember_PUSH_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_DELETE = CLONE access restriction, DELETE access permission + */ + @Test + public void testTeamMember_CLONE_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_DELETE = VIEW access restriction, DELETE access permission + */ + @Test + public void testTeamMember_VIEW_DELETE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.DELETE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * NONE_REWIND = NO access restriction, REWIND access permission + * (not useful scenario) + */ + @Test + public void testTeamMember_NONE_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_REWIND = PUSH access restriction, REWIND access permission + */ + @Test + public void testTeamMember_PUSH_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_REWIND = CLONE access restriction, REWIND access permission + */ + @Test + public void testTeamMember_CLONE_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_REWIND = VIEW access restriction, REWIND access permission + */ + @Test + public void testTeamMember_VIEW_REWIND() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.REWIND); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * NONE_CLONE = NO access restriction, CLONE access permission + * (not useful scenario) + */ + @Test + public void testTeamMember_NONE_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_CLONE = PUSH access restriction, CLONE access permission + */ + @Test + public void testTeamMember_PUSH_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_CLONE = CLONE access restriction, CLONE access permission + */ + @Test + public void testTeamMember_CLONE_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_CLONE = VIEW access restriction, CLONE access permission + */ + @Test + public void testTeamMember_VIEW_CLONE() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.CLONE); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * NONE_VIEW = NO access restriction, VIEW access permission + * (not useful scenario) + */ + @Test + public void testTeamMember_NONE_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.NONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertTrue("team member CAN NOT push!", user.canPush(repository)); + + assertTrue("team member CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("team member CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("team member CAN NOT rewind ref!", user.canRewindRef(repository)); + } + + /** + * PUSH_VIEW = PUSH access restriction, VIEW access permission + */ + @Test + public void testTeamMember_PUSH_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.PUSH; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertTrue("team member CAN NOT clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * CLONE_VIEW = CLONE access restriction, VIEW access permission + */ + @Test + public void testTeamMember_CLONE_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.CLONE; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertFalse("team member CAN clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + /** + * VIEW_VIEW = VIEW access restriction, VIEW access permission + */ + @Test + public void testTeamMember_VIEW_VIEW() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + TeamModel team = new TeamModel("test"); + team.setRepositoryPermission(repository.name, AccessPermission.VIEW); + UserModel user = new UserModel("test"); + user.teams.add(team); + + assertTrue("team member CAN NOT view!", user.canView(repository)); + assertFalse("team member CAN clone!", user.canClone(repository)); + assertFalse("team member CAN push!", user.canPush(repository)); + + assertFalse("team member CAN create ref!", user.canCreateRef(repository)); + assertFalse("team member CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("team member CAN rewind ref!", user.canRewindRef(repository)); + } + + @Test + public void testOwner() throws Exception { + RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + repository.owner = user.username; + + assertTrue("owner CAN NOT view!", user.canView(repository)); + assertTrue("owner CAN NOT clone!", user.canClone(repository)); + assertTrue("owner CAN NOT push!", user.canPush(repository)); + + assertTrue("owner CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("owner CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("owner CAN NOT rewind ref!", user.canRewindRef(repository)); + + assertTrue("owner CAN NOT fork!", user.canFork(repository)); + + assertFalse("owner CAN NOT delete!", user.canDelete(repository)); + assertTrue("owner CAN NOT edit!", user.canEdit(repository)); + } + + @Test + public void testOwnerPersonalRepository() throws Exception { + RepositoryModel repository = new RepositoryModel("~test/myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("test"); + repository.owner = user.username; + + assertTrue("user CAN NOT view!", user.canView(repository)); + assertTrue("user CAN NOT clone!", user.canClone(repository)); + assertTrue("user CAN NOT push!", user.canPush(repository)); + + assertTrue("user CAN NOT create ref!", user.canCreateRef(repository)); + assertTrue("user CAN NOT delete ref!", user.canDeleteRef(repository)); + assertTrue("user CAN NOT rewind ref!", user.canRewindRef(repository)); + + assertFalse("user CAN fork!", user.canFork(repository)); + + assertTrue("user CAN NOT delete!", user.canDelete(repository)); + assertTrue("user CAN NOT edit!", user.canEdit(repository)); + } + + @Test + public void testVisitorPersonalRepository() throws Exception { + RepositoryModel repository = new RepositoryModel("~test/myrepo.git", null, null, new Date()); + repository.authorizationControl = AuthorizationControl.NAMED; + repository.accessRestriction = AccessRestrictionType.VIEW; + + UserModel user = new UserModel("visitor"); + repository.owner = "test"; + + assertFalse("user CAN view!", user.canView(repository)); + assertFalse("user CAN clone!", user.canClone(repository)); + assertFalse("user CAN push!", user.canPush(repository)); + + assertFalse("user CAN create ref!", user.canCreateRef(repository)); + assertFalse("user CAN delete ref!", user.canDeleteRef(repository)); + assertFalse("user CAN rewind ref!", user.canRewindRef(repository)); + + assertFalse("user CAN fork!", user.canFork(repository)); + + assertFalse("user CAN delete!", user.canDelete(repository)); + assertFalse("user CAN edit!", user.canEdit(repository)); + } +} diff --git a/tests/com/gitblit/tests/RpcTests.java b/tests/com/gitblit/tests/RpcTests.java index 1080849c..3ad0ec59 100644 --- a/tests/com/gitblit/tests/RpcTests.java +++ b/tests/com/gitblit/tests/RpcTests.java @@ -247,7 +247,7 @@ public class RpcTests { // Create the A-Team TeamModel aTeam = new TeamModel("A-Team"); aTeam.users.add("admin"); - aTeam.repositories.add("helloworld.git"); + aTeam.addRepositoryPermission("helloworld.git"); assertTrue(RpcUtils.createTeam(aTeam, url, account, password.toCharArray())); aTeam = null; @@ -261,7 +261,7 @@ public class RpcTests { } assertNotNull(aTeam); assertTrue(aTeam.hasUser("admin")); - assertTrue(aTeam.hasRepository("helloworld.git")); + assertTrue(aTeam.hasRepositoryPermission("helloworld.git")); RepositoryModel helloworld = null; Map repositories = RpcUtils.getRepositories(url, account, diff --git a/tests/com/gitblit/tests/UserServiceTest.java b/tests/com/gitblit/tests/UserServiceTest.java index 03051bdb..710d1f35 100644 --- a/tests/com/gitblit/tests/UserServiceTest.java +++ b/tests/com/gitblit/tests/UserServiceTest.java @@ -25,8 +25,10 @@ import java.io.IOException; import org.junit.Test; import com.gitblit.ConfigUserService; +import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.FileUserService; import com.gitblit.IUserService; +import com.gitblit.models.RepositoryModel; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; @@ -74,9 +76,9 @@ public class UserServiceTest { // add new user UserModel newUser = new UserModel("test"); newUser.password = "testPassword"; - newUser.addRepository("repo1"); - newUser.addRepository("repo2"); - newUser.addRepository("sub/repo3"); + newUser.addRepositoryPermission("repo1"); + newUser.addRepositoryPermission("repo2"); + newUser.addRepositoryPermission("sub/repo3"); service.updateUserModel(newUser); // add one more new user and then test reload of first new user @@ -93,10 +95,10 @@ public class UserServiceTest { // confirm reloaded test user newUser = service.getUserModel("test"); assertEquals("testPassword", newUser.password); - assertEquals(3, newUser.repositories.size()); - assertTrue(newUser.hasRepository("repo1")); - assertTrue(newUser.hasRepository("repo2")); - assertTrue(newUser.hasRepository("sub/repo3")); + assertEquals(3, newUser.permissions.size()); + assertTrue(newUser.hasRepositoryPermission("repo1")); + assertTrue(newUser.hasRepositoryPermission("repo2")); + assertTrue(newUser.hasRepositoryPermission("sub/repo3")); // confirm authentication of test user UserModel testUser = service.authenticate("test", "testPassword".toCharArray()); @@ -106,7 +108,7 @@ public class UserServiceTest { // delete a repository role and confirm role removal from test user service.deleteRepositoryRole("repo2"); testUser = service.getUserModel("test"); - assertEquals(2, testUser.repositories.size()); + assertEquals(2, testUser.permissions.size()); // delete garbage user and confirm user count service.deleteUser("garbage"); @@ -115,7 +117,7 @@ public class UserServiceTest { // rename repository and confirm role change for test user service.renameRepositoryRole("repo1", "newrepo1"); testUser = service.getUserModel("test"); - assertTrue(testUser.hasRepository("newrepo1")); + assertTrue(testUser.hasRepositoryPermission("newrepo1")); } protected void testTeams(IUserService service) { @@ -123,41 +125,51 @@ public class UserServiceTest { // confirm we have 1 team (admins) assertEquals(1, service.getAllTeamNames().size()); assertEquals("admins", service.getAllTeamNames().get(0)); + + RepositoryModel newrepo1 = new RepositoryModel("newrepo1", null, null, null); + newrepo1.accessRestriction = AccessRestrictionType.VIEW; + RepositoryModel NEWREPO1 = new RepositoryModel("NEWREPO1", null, null, null); + NEWREPO1.accessRestriction = AccessRestrictionType.VIEW; // remove newrepo1 from test user // now test user has no repositories UserModel user = service.getUserModel("test"); - user.repositories.clear(); + user.permissions.clear(); service.updateUserModel(user); user = service.getUserModel("test"); - assertEquals(0, user.repositories.size()); - assertFalse(user.canAccessRepository("newrepo1")); - assertFalse(user.canAccessRepository("NEWREPO1")); + assertEquals(0, user.permissions.size()); + assertFalse(user.canView(newrepo1)); + assertFalse(user.canView(NEWREPO1)); // create test team and add test user and newrepo1 TeamModel team = new TeamModel("testteam"); team.addUser("test"); - team.addRepository("newrepo1"); + team.addRepositoryPermission(newrepo1.name); service.updateTeamModel(team); // confirm 1 user and 1 repo team = service.getTeamModel("testteam"); - assertEquals(1, team.repositories.size()); + assertEquals(1, team.permissions.size()); assertEquals(1, team.users.size()); // confirm team membership user = service.getUserModel("test"); - assertEquals(0, user.repositories.size()); + assertEquals(0, user.permissions.size()); assertEquals(1, user.teams.size()); // confirm team access - assertTrue(team.hasRepository("newrepo1")); - assertTrue(user.hasTeamAccess("newrepo1")); - assertTrue(team.hasRepository("NEWREPO1")); - assertTrue(user.hasTeamAccess("NEWREPO1")); + assertTrue(team.hasRepositoryPermission(newrepo1.name)); + assertTrue(user.canView(newrepo1)); + assertTrue(team.hasRepositoryPermission(NEWREPO1.name)); + assertTrue(user.canView(NEWREPO1)); // rename the team and add new repository - team.addRepository("newrepo2"); + RepositoryModel newrepo2 = new RepositoryModel("newrepo2", null, null, null); + newrepo2.accessRestriction = AccessRestrictionType.VIEW; + RepositoryModel NEWREPO2 = new RepositoryModel("NEWREPO2", null, null, null); + NEWREPO2.accessRestriction = AccessRestrictionType.VIEW; + + team.addRepositoryPermission(newrepo2.name); team.name = "testteam2"; service.updateTeamModel("testteam", team); @@ -165,11 +177,11 @@ public class UserServiceTest { user = service.getUserModel("test"); // confirm user and team can access newrepo2 - assertEquals(2, team.repositories.size()); - assertTrue(team.hasRepository("newrepo2")); - assertTrue(user.hasTeamAccess("newrepo2")); - assertTrue(team.hasRepository("NEWREPO2")); - assertTrue(user.hasTeamAccess("NEWREPO2")); + assertEquals(2, team.permissions.size()); + assertTrue(team.hasRepositoryPermission(newrepo2.name)); + assertTrue(user.canView(newrepo2)); + assertTrue(team.hasRepositoryPermission(NEWREPO2.name)); + assertTrue(user.canView(NEWREPO2)); // delete testteam2 service.deleteTeam("testteam2"); @@ -178,28 +190,28 @@ public class UserServiceTest { // confirm team does not exist and user can not access newrepo1 and 2 assertEquals(null, team); - assertFalse(user.canAccessRepository("newrepo1")); - assertFalse(user.canAccessRepository("newrepo2")); + assertFalse(user.canView(newrepo1)); + assertFalse(user.canView(newrepo2)); // create new team and add it to user // this tests the inverse team creation/team addition team = new TeamModel("testteam"); - team.addRepository("NEWREPO1"); - team.addRepository("NEWREPO2"); + team.addRepositoryPermission(NEWREPO1.name); + team.addRepositoryPermission(NEWREPO2.name); user.teams.add(team); service.updateUserModel(user); // confirm the inverted team addition user = service.getUserModel("test"); team = service.getTeamModel("testteam"); - assertTrue(user.hasTeamAccess("newrepo1")); - assertTrue(user.hasTeamAccess("newrepo2")); + assertTrue(user.canView(newrepo1)); + assertTrue(user.canView(newrepo2)); assertTrue(team.hasUser("test")); // drop testteam from user and add nextteam to user team = new TeamModel("nextteam"); - team.addRepository("NEWREPO1"); - team.addRepository("NEWREPO2"); + team.addRepositoryPermission(NEWREPO1.name); + team.addRepositoryPermission(NEWREPO2.name); user.teams.clear(); user.teams.add(team); service.updateUserModel(user); @@ -207,8 +219,8 @@ public class UserServiceTest { // confirm implicit drop user = service.getUserModel("test"); team = service.getTeamModel("testteam"); - assertTrue(user.hasTeamAccess("newrepo1")); - assertTrue(user.hasTeamAccess("newrepo2")); + assertTrue(user.canView(newrepo1)); + assertTrue(user.canView(newrepo2)); assertFalse(team.hasUser("test")); team = service.getTeamModel("nextteam"); assertTrue(team.hasUser("test")); -- 2.39.5