##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r4251:fc351abb merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,60 b''
1 |RCE| 4.18.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-01-20
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - API: invalidate license cache on set_license_key call.
19 - API: add send_email flag for comments api to allow commenting without email notification.
20 - API: added pull requests versions into returned API data.
21 - Dashboard: fixed jumping of text in grid loading by new loading indicator.
22 - Installation: add few extra defaults that makes RhodeCode nicer out of the box.
23 - Pull Requests: small code cleanup to define other type of merge username.
24 RC_MERGE_USER_NAME_ATTR env variable defines what should be used from user as merge username.
25 - Gists: cleanup UI and make the gist access id use monospace according to the new UI.
26
27
28 Security
29 ^^^^^^^^
30
31 - Repository permission: properly flush permission caches on set private mode of repository.
32 Otherwise we get cached values still in place until it expires.
33 - Repository permission: add set/un-set of private repository from permissions page.
34 - Permissions: flush all user permissions in case of default user permission changes.
35
36
37 Performance
38 ^^^^^^^^^^^
39
40 - Caches: used more efficient way of fetching all users for permissions invalidation.
41 - Issue trackers: optimized performance of fetching issue tracker patterns.
42
43
44 Fixes
45 ^^^^^
46
47 - SSH: fixed SSH problems with EE edition.
48 - Branch permissions: remove emtpy tooltips on branch permission entries.
49 - Core: fixed cython compat inspect that caused some API calls to not work correctly in EE release.
50 - Audit logger: use copy of params we later modify to prevent from modification by the store
51 function of parameters that we only use for reading.
52 - Users: fixed wrong mention of readme in user description help block.
53 - Issue trackers: fixed wrong examples in patterns.
54 - Issue trackers: fixed missing option to get back to inherited settings.
55
56
57 Upgrade notes
58 ^^^^^^^^^^^^^
59
60 - Scheduled release addressing problems in 4.18.X releases.
@@ -0,0 +1,49 b''
1 |RCE| 4.18.2 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-01-28
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - Permissions: add better help text about default permissions, and correlation with anonymous access enabled.
19 - Mentions: markdown renderer now wraps username in hovercard logic allowing checking the mentioned user.
20 - Documentation: added note about hard restart due to celery update.
21 - Maintenance: run rebuildfncache for Mercurial in maintenance command.
22
23
24 Security
25 ^^^^^^^^
26
27
28
29 Performance
30 ^^^^^^^^^^^
31
32 - Authentication: cache plugins for auth and their settings in the auth_registry for single request.
33 This heavily influences SVN performance on multiple-file commits.
34
35
36 Fixes
37 ^^^^^
38
39 - Descriptions: fixed rendering problem with certain meta-tags in repo description.
40 - Emails: fixed fonts rendering problems in Outlook.
41 - Emails: fixed bug in test email sending.
42 - Summary: fixed styling of readme indicator.
43 - Flash: fixed display problem with flash messages on error pages.
44
45
46 Upgrade notes
47 ^^^^^^^^^^^^^
48
49 - Scheduled release addressing problems in 4.18.X releases.
@@ -0,0 +1,64 b''
1 |RCE| 4.18.3 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-03-24
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - LDAP: added nested user groups sync which was planned in 4.18.X but didn't
14 make it to the release. New option for sync is available in the LDAP configuration.
15
16
17 General
18 ^^^^^^^
19
20 - API: added branch permissions functions.
21 - Pull requests: added creating indicator to let users know they should wait until PR is creating.
22 - Pull requests: allow super-admins to force change state of locked PRs.
23 - Users/User groups: in edit mode we now show the actual name of what we're editing.
24 - SSH: allow generation of legacy SSH keys for older systems and Windows users.
25 - File store: don't response with cookie data on file-store download response.
26 - File store: use our own logic for setting content-type. This solves a problem
27 when previously used resolver set different content-type+content-encoding which
28 is an incorrect behaviour.
29 - My Account: show info about password usage for external accounts e.g github/google etc
30 We now recommend using auth-tokens instead of actual passwords.
31 - Repositories: in description field we now show mention of metatags only if they
32 are enabled.
33
34
35 Security
36 ^^^^^^^^
37
38 - Remote sync: don't expose credentials in displayed URLs.
39 Remote links url had visible credentials displayed in the link.
40 This was used for web-view and not needed anymore.
41
42
43 Performance
44 ^^^^^^^^^^^
45
46 - Full text search: significantly improved GIT commit indexing performance by reducing
47 number of calls to the vcsserver.
48
49
50 Fixes
51 ^^^^^
52
53 - Mercurial: fixed cases of lookup of branches that are exactly 20 character long.
54 - SVN: allow legacy (pre SVN 1.7) extraction of post commit data.
55 - GIT: use non-unicode author extraction as it's returned as bytes from backend, and
56 we can get an unicode errors while there's some non-ascii characters.
57 - GIT: use safe configparser for git submodules to prevent from errors on submodules with % sign.
58 - System info: fixed UI problem with new version update info screen.
59
60
61 Upgrade notes
62 ^^^^^^^^^^^^^
63
64 - Scheduled release addressing problems in 4.18.X releases.
@@ -1,60 +1,64 b''
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
61 f37a3126570477543507f0bc9d245ce75546181a v4.18.0
62 71d8791463e87b64c1a18475de330ee600d37561 v4.18.1
63 4bd6b75dac1d25c64885d4d49385e5533f21c525 v4.18.2
64 12ed92fe57f2e9fc7b71dc0b65e26c2da5c7085f v4.18.3
@@ -1,61 +1,61 b''
1 .. _repo-admin-tasks:
1 .. _repo-admin-tasks:
2
2
3 Common Admin Tasks for Repositories
3 Common Admin Tasks for Repositories
4 -----------------------------------
4 -----------------------------------
5
5
6
6
7 Manually Force Delete Repository
7 Manually Force Delete Repository
8 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9
9
10 In case of attached forks or pull-requests repositories should be archived.
10 In case of attached forks or pull-requests repositories should be archived.
11 Here is how to force delete a repository and remove all dependent objects
11 Here is how to force delete a repository and remove all dependent objects
12
12
13
13
14 .. code-block:: bash
14 .. code-block:: bash
15 :dedent: 1
15 :dedent: 1
16
16
17 # starts the ishell interactive prompt
17 # starts the ishell interactive prompt
18 $ rccontrol ishell enterprise-1
18 $ rccontrol ishell enterprise-1
19
19
20 .. code-block:: python
20 .. code-block:: python
21 :dedent: 1
21 :dedent: 1
22
22
23 In [4]: from rhodecode.model.repo import RepoModel
23 In [4]: from rhodecode.model.repo import RepoModel
24 In [3]: repo = Repository.get_by_repo_name('test_repos/repo_with_prs')
24 In [3]: repo = Repository.get_by_repo_name('test_repos/repo_with_prs')
25 In [5]: RepoModel().delete(repo, forks='detach', pull_requests='delete')
25 In [5]: RepoModel().delete(repo, forks='detach', pull_requests='delete')
26 In [6]: Session().commit()
26 In [6]: Session().commit()
27
27
28
28
29 Below is a fully automated example to force delete repositories reading from a
29 Below is a fully automated example to force delete repositories reading from a
30 file where each line is a repository name. This can be executed via simple CLI command
30 file where each line is a repository name. This can be executed via simple CLI command
31 without entering the interactive shell.
31 without entering the interactive shell.
32
32
33 Save the below content as a file named `repo_delete_task.py`
33 Save the below content as a file named `repo_delete_task.py`
34
34
35
35
36 .. code-block:: python
36 .. code-block:: python
37 :dedent: 1
37 :dedent: 1
38
38
39 from rhodecode.model.db import *
39 from rhodecode.model.db import *
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 with open('delete_repos.txt', 'rb') as f:
41 with open('delete_repos.txt', 'rb') as f:
42 # read all lines from file
42 # read all lines from file
43 repos = f.readlines()
43 repos = f.readlines()
44 for repo_name in repos:
44 for repo_name in repos:
45 repo_name = repo_name.strip() # cleanup the name just in case
45 repo_name = repo_name.strip() # cleanup the name just in case
46 repo = Repository.get_by_repo_name(repo_name)
46 repo = Repository.get_by_repo_name(repo_name)
47 if not repo:
47 if not repo:
48 raise Exception('Repo with name {} not found'.format(repo_name))
48 raise Exception('Repo with name {} not found'.format(repo_name))
49 RepoModel().delete(repo, forks='detach', pull_requests='delete')
49 RepoModel().delete(repo, forks='detach', pull_requests='delete')
50 Session().commit()
50 Session().commit()
51 print('Removed repository {}'.format(repo_name))
51 print('Removed repository {}'.format(repo_name))
52
52
53
53
54 The code above will read the names of repositories from a file called `delete_repos.txt`
54 The code above will read the names of repositories from a file called `delete_repos.txt`
55 Each lines should represent a single name e.g `repo_name_1` or `repo_group/repo_name_2`
55 Each lines should represent a single name e.g `repo_name_1` or `repo_group/repo_name_2`
56
56
57 Run this line from CLI to execute the code from the `repo_delete_task.py` file and
57 Run this line from CLI to execute the code from the `repo_delete_task.py` file and
58 exit the ishell after the execution::
58 exit the ishell after the execution::
59
59
60 echo "%run repo_delete_task.py" | rccontrol ishell Enterprise-1
60 echo "%run repo_delete_task.py" | rccontrol ishell enterprise-1
61
61
@@ -1,207 +1,208 b''
1 .. _svn-http:
1 .. _svn-http:
2
2
3 |svn| With Write Over HTTP
3 |svn| With Write Over HTTP
4 ^^^^^^^^^^^^^^^^^^^^^^^^^^
4 ^^^^^^^^^^^^^^^^^^^^^^^^^^
5
5
6 To use |svn| with read/write support over the |svn| HTTP protocol, you have to
6 To use |svn| with read/write support over the |svn| HTTP protocol, you have to
7 configure the HTTP |svn| backend.
7 configure the HTTP |svn| backend.
8
8
9 Prerequisites
9 Prerequisites
10 =============
10 =============
11
11
12 - Enable HTTP support inside the admin VCS settings on your |RCE| instance
12 - Enable HTTP support inside the admin VCS settings on your |RCE| instance
13 - You need to install the following tools on the machine that is running an
13 - You need to install the following tools on the machine that is running an
14 instance of |RCE|:
14 instance of |RCE|:
15 ``Apache HTTP Server`` and ``mod_dav_svn``.
15 ``Apache HTTP Server`` and ``mod_dav_svn``.
16
16
17
17
18 .. tip::
18 .. tip::
19
19
20 We recommend using Wandisco repositories which provide latest SVN versions
20 We recommend using Wandisco repositories which provide latest SVN versions
21 for most platforms. If you skip this version you'll have to ensure the Client version
21 for most platforms. If you skip this version you'll have to ensure the Client version
22 is compatible with installed SVN version which might differ depending on the operating system.
22 is compatible with installed SVN version which might differ depending on the operating system.
23 Here is an example how to add the Wandisco repositories for Ubuntu.
23 Here is an example how to add the Wandisco repositories for Ubuntu.
24
24
25 .. code-block:: bash
25 .. code-block:: bash
26
26
27 $ sudo sh -c 'echo "deb http://opensource.wandisco.com/ubuntu `lsb_release -cs` svn110" >> /etc/apt/sources.list.d/subversion110.list'
27 $ sudo sh -c 'echo "deb http://opensource.wandisco.com/ubuntu `lsb_release -cs` svn110" >> /etc/apt/sources.list.d/subversion110.list'
28 $ sudo wget -q http://opensource.wandisco.com/wandisco-debian-new.gpg -O- | sudo apt-key add -
28 $ sudo wget -q http://opensource.wandisco.com/wandisco-debian-new.gpg -O- | sudo apt-key add -
29 $ sudo apt-get update
29 $ sudo apt-get update
30
30
31 Here is an example how to add the Wandisco repositories for Centos/Redhat. Using
31 Here is an example how to add the Wandisco repositories for Centos/Redhat. Using
32 a yum config
32 a yum config
33
33
34 .. code-block:: bash
34 .. code-block:: bash
35
35
36 [wandisco-Git]
36 [wandisco-Git]
37 name=CentOS-6 - Wandisco Git
37 name=CentOS-6 - Wandisco Git
38 baseurl=http://opensource.wandisco.com/centos/6/git/$basearch/
38 baseurl=http://opensource.wandisco.com/centos/6/git/$basearch/
39 enabled=1
39 enabled=1
40 gpgcheck=1
40 gpgcheck=1
41 gpgkey=http://opensource.wandisco.com/RPM-GPG-KEY-WANdisco
41 gpgkey=http://opensource.wandisco.com/RPM-GPG-KEY-WANdisco
42
42
43
43
44
44
45 Example installation of required components for Ubuntu platform:
45 Example installation of required components for Ubuntu platform:
46
46
47 .. code-block:: bash
47 .. code-block:: bash
48
48
49 $ sudo apt-get install apache2
49 $ sudo apt-get install apache2
50 $ sudo apt-get install libapache2-svn
50 $ sudo apt-get install libapache2-svn
51
51
52 Once installed you need to enable ``dav_svn`` on Ubuntu:
52 Once installed you need to enable ``dav_svn`` on Ubuntu:
53
53
54 .. code-block:: bash
54 .. code-block:: bash
55
55
56 $ sudo a2enmod dav_svn
56 $ sudo a2enmod dav_svn
57 $ sudo a2enmod headers
57 $ sudo a2enmod headers
58 $ sudo a2enmod authn_anon
58 $ sudo a2enmod authn_anon
59
59
60
60
61 Example installation of required components for RedHat/CentOS platform:
61 Example installation of required components for RedHat/CentOS platform:
62
62
63 .. code-block:: bash
63 .. code-block:: bash
64
64
65 $ sudo yum install httpd
65 $ sudo yum install httpd
66 $ sudo yum install subversion mod_dav_svn
66 $ sudo yum install subversion mod_dav_svn
67
67
68
68
69 Once installed you need to enable ``dav_svn`` on RedHat/CentOS:
69 Once installed you need to enable ``dav_svn`` on RedHat/CentOS:
70
70
71 .. code-block:: bash
71 .. code-block:: bash
72
72
73 sudo vi /etc/httpd/conf.modules.d/10-subversion.conf
73 sudo vi /etc/httpd/conf.modules.d/10-subversion.conf
74 ## The file should read:
74 ## The file should read:
75
75
76 LoadModule dav_svn_module modules/mod_dav_svn.so
76 LoadModule dav_svn_module modules/mod_dav_svn.so
77 LoadModule headers_module modules/mod_headers.so
77 LoadModule headers_module modules/mod_headers.so
78 LoadModule authn_anon_module modules/mod_authn_anon.so
78 LoadModule authn_anon_module modules/mod_authn_anon.so
79
79
80 .. tip::
80 .. tip::
81
81
82 To check the installed mod_dav_svn module version, you can use such command.
82 To check the installed mod_dav_svn module version, you can use such command.
83
83
84 `strings /usr/lib/apache2/modules/mod_dav_svn.so | grep 'Powered by'`
84 `strings /usr/lib/apache2/modules/mod_dav_svn.so | grep 'Powered by'`
85
85
86
86
87 Configuring Apache Setup
87 Configuring Apache Setup
88 ========================
88 ========================
89
89
90 .. tip::
90 .. tip::
91
91
92 It is recommended to run Apache on a port other than 80, due to possible
92 It is recommended to run Apache on a port other than 80, due to possible
93 conflicts with other HTTP servers like nginx. To do this, set the
93 conflicts with other HTTP servers like nginx. To do this, set the
94 ``Listen`` parameter in the ``/etc/apache2/ports.conf`` file, for example
94 ``Listen`` parameter in the ``/etc/apache2/ports.conf`` file, for example
95 ``Listen 8090``.
95 ``Listen 8090``.
96
96
97
97
98 .. warning::
98 .. warning::
99
99
100 Make sure your Apache instance which runs the mod_dav_svn module is
100 Make sure your Apache instance which runs the mod_dav_svn module is
101 only accessible by |RCE|. Otherwise everyone is able to browse
101 only accessible by |RCE|. Otherwise everyone is able to browse
102 the repositories or run subversion operations (checkout/commit/etc.).
102 the repositories or run subversion operations (checkout/commit/etc.).
103
103
104 It is also recommended to run apache as the same user as |RCE|, otherwise
104 It is also recommended to run apache as the same user as |RCE|, otherwise
105 permission issues could occur. To do this edit the ``/etc/apache2/envvars``
105 permission issues could occur. To do this edit the ``/etc/apache2/envvars``
106
106
107 .. code-block:: apache
107 .. code-block:: apache
108
108
109 export APACHE_RUN_USER=rhodecode
109 export APACHE_RUN_USER=rhodecode
110 export APACHE_RUN_GROUP=rhodecode
110 export APACHE_RUN_GROUP=rhodecode
111
111
112 1. To configure Apache, create and edit a virtual hosts file, for example
112 1. To configure Apache, create and edit a virtual hosts file, for example
113 :file:`/etc/apache2/sites-enabled/default.conf`. Below is an example
113 :file:`/etc/apache2/sites-enabled/default.conf`. Below is an example
114 how to use one with auto-generated config ```mod_dav_svn.conf```
114 how to use one with auto-generated config ```mod_dav_svn.conf```
115 from configured |RCE| instance.
115 from configured |RCE| instance.
116
116
117 .. code-block:: apache
117 .. code-block:: apache
118
118
119 <VirtualHost *:8090>
119 <VirtualHost *:8090>
120 ServerAdmin rhodecode-admin@localhost
120 ServerAdmin rhodecode-admin@localhost
121 DocumentRoot /var/www/html
121 DocumentRoot /var/www/html
122 ErrorLog ${'${APACHE_LOG_DIR}'}/error.log
122 ErrorLog ${'${APACHE_LOG_DIR}'}/error.log
123 CustomLog ${'${APACHE_LOG_DIR}'}/access.log combined
123 CustomLog ${'${APACHE_LOG_DIR}'}/access.log combined
124 LogLevel info
124 LogLevel info
125 # allows custom host names, prevents 400 errors on checkout
125 # allows custom host names, prevents 400 errors on checkout
126 HttpProtocolOptions Unsafe
126 HttpProtocolOptions Unsafe
127 # Most likely this will be: /home/user/.rccontrol/enterprise-1/mod_dav_svn.conf
127 Include /home/user/.rccontrol/enterprise-1/mod_dav_svn.conf
128 Include /home/user/.rccontrol/enterprise-1/mod_dav_svn.conf
128 </VirtualHost>
129 </VirtualHost>
129
130
130
131
131 2. Go to the :menuselection:`Admin --> Settings --> VCS` page, and
132 2. Go to the :menuselection:`Admin --> Settings --> VCS` page, and
132 enable :guilabel:`Proxy Subversion HTTP requests`, and specify the
133 enable :guilabel:`Proxy Subversion HTTP requests`, and specify the
133 :guilabel:`Subversion HTTP Server URL`.
134 :guilabel:`Subversion HTTP Server URL`.
134
135
135 3. Open the |RCE| configuration file,
136 3. Open the |RCE| configuration file,
136 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
137 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
137
138
138 4. Add the following configuration option in the ``[app:main]``
139 4. Add the following configuration option in the ``[app:main]``
139 section if you don't have it yet.
140 section if you don't have it yet.
140
141
141 This enables mapping of the created |RCE| repo groups into special
142 This enables mapping of the created |RCE| repo groups into special
142 |svn| paths. Each time a new repository group is created, the system will
143 |svn| paths. Each time a new repository group is created, the system will
143 update the template file and create new mapping. Apache web server needs to
144 update the template file and create new mapping. Apache web server needs to
144 be reloaded to pick up the changes on this file.
145 be reloaded to pick up the changes on this file.
145 To do this, simply configure `svn.proxy.reload_cmd` inside the .ini file.
146 To do this, simply configure `svn.proxy.reload_cmd` inside the .ini file.
146 Example configuration:
147 Example configuration:
147
148
148
149
149 .. code-block:: ini
150 .. code-block:: ini
150
151
151 ############################################################
152 ############################################################
152 ### Subversion proxy support (mod_dav_svn) ###
153 ### Subversion proxy support (mod_dav_svn) ###
153 ### Maps RhodeCode repo groups into SVN paths for Apache ###
154 ### Maps RhodeCode repo groups into SVN paths for Apache ###
154 ############################################################
155 ############################################################
155 ## Enable or disable the config file generation.
156 ## Enable or disable the config file generation.
156 svn.proxy.generate_config = true
157 svn.proxy.generate_config = true
157 ## Generate config file with `SVNListParentPath` set to `On`.
158 ## Generate config file with `SVNListParentPath` set to `On`.
158 svn.proxy.list_parent_path = true
159 svn.proxy.list_parent_path = true
159 ## Set location and file name of generated config file.
160 ## Set location and file name of generated config file.
160 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
161 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
161 ## Used as a prefix to the <Location> block in the generated config file.
162 ## Used as a prefix to the <Location> block in the generated config file.
162 ## In most cases it should be set to `/`.
163 ## In most cases it should be set to `/`.
163 svn.proxy.location_root = /
164 svn.proxy.location_root = /
164 ## Command to reload the mod dav svn configuration on change.
165 ## Command to reload the mod dav svn configuration on change.
165 ## Example: `/etc/init.d/apache2 reload`
166 ## Example: `/etc/init.d/apache2 reload`
166 svn.proxy.reload_cmd = /etc/init.d/apache2 reload
167 svn.proxy.reload_cmd = /etc/init.d/apache2 reload
167 ## If the timeout expires before the reload command finishes, the command will
168 ## If the timeout expires before the reload command finishes, the command will
168 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
169 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
169 #svn.proxy.reload_timeout = 10
170 #svn.proxy.reload_timeout = 10
170
171
171
172
172 This would create a special template file called ```mod_dav_svn.conf```. We
173 This would create a special template file called ```mod_dav_svn.conf```. We
173 used that file path in the apache config above inside the Include statement.
174 used that file path in the apache config above inside the Include statement.
174 It's also possible to manually generate the config from the
175 It's also possible to manually generate the config from the
175 :menuselection:`Admin --> Settings --> VCS` page by clicking a
176 :menuselection:`Admin --> Settings --> VCS` page by clicking a
176 `Generate Apache Config` button.
177 `Generate Apache Config` button.
177
178
178 5. Now only things left is to enable svn support, and generate the initial
179 5. Now only things left is to enable svn support, and generate the initial
179 configuration.
180 configuration.
180
181
181 - Select `Proxy subversion HTTP requests` checkbox
182 - Select `Proxy subversion HTTP requests` checkbox
182 - Enter http://localhost:8090 into `Subversion HTTP Server URL`
183 - Enter http://localhost:8090 into `Subversion HTTP Server URL`
183 - Click the `Generate Apache Config` button.
184 - Click the `Generate Apache Config` button.
184
185
185 This config will be automatically re-generated once an user-groups is added
186 This config will be automatically re-generated once an user-groups is added
186 to properly map the additional paths generated.
187 to properly map the additional paths generated.
187
188
188
189
189
190
190 Using |svn|
191 Using |svn|
191 ===========
192 ===========
192
193
193 Once |svn| has been enabled on your instance, you can use it with the
194 Once |svn| has been enabled on your instance, you can use it with the
194 following examples. For more |svn| information, see the `Subversion Red Book`_
195 following examples. For more |svn| information, see the `Subversion Red Book`_
195
196
196 .. code-block:: bash
197 .. code-block:: bash
197
198
198 # To clone a repository
199 # To clone a repository
199 svn checkout http://my-svn-server.example.com/my-svn-repo
200 svn checkout http://my-svn-server.example.com/my-svn-repo
200
201
201 # svn commit
202 # svn commit
202 svn commit
203 svn commit
203
204
204
205
205 .. _Subversion Red Book: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.ref.svn
206 .. _Subversion Red Book: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.ref.svn
206
207
207 .. _Ask Ubuntu: http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue No newline at end of file
208 .. _Ask Ubuntu: http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue
@@ -1,67 +1,67 b''
1 .. _user-session-ref:
1 .. _user-session-ref:
2
2
3 User Session Performance
3 User Session Performance
4 ------------------------
4 ------------------------
5
5
6 The default file-based sessions are only suitable for smaller setups, or
6 The default file-based sessions are only suitable for smaller setups, or
7 instances that doesn't have a lot of users or traffic.
7 instances that doesn't have a lot of users or traffic.
8 They are set as default option because it's setup-free solution.
8 They are set as default option because it's setup-free solution.
9
9
10 The most common issue of file based sessions are file limit errors which occur
10 The most common issue of file based sessions are file limit errors which occur
11 if there are lots of session files.
11 if there are lots of session files.
12
12
13 Therefore, in a large scale deployment, to give better performance,
13 Therefore, in a large scale deployment, to give better performance,
14 scalability, and maintainability we recommend switching from file-based
14 scalability, and maintainability we recommend switching from file-based
15 sessions to database-based user sessions or Redis based sessions.
15 sessions to database-based user sessions or Redis based sessions.
16
16
17 To switch to database-based user sessions uncomment the following section in
17 To switch to database-based user sessions uncomment the following section in
18 your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
18 your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
19
19
20
20
21 .. code-block:: ini
21 .. code-block:: ini
22
22
23 ## db based session, fast, and allows easy management over logged in users
23 ## db based session, fast, and allows easy management over logged in users
24 beaker.session.type = ext:database
24 beaker.session.type = ext:database
25 beaker.session.table_name = db_session
25 beaker.session.table_name = db_session
26
26
27 # use just one of the following according to the type of database
27 # use just one of the following according to the type of database
28 beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
28 beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
29 # or
29 # or
30 beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
30 beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
31
31
32 beaker.session.sa.pool_recycle = 3600
32 beaker.session.sa.pool_recycle = 3600
33 beaker.session.sa.echo = false
33 beaker.session.sa.echo = false
34
34
35
35
36 and make sure you comment out the file based sessions.
36 and make sure you comment out the file based sessions.
37
37
38 .. code-block:: ini
38 .. code-block:: ini
39
39
40 ## types are file, ext:memcached, ext:database, and memory (default).
40 ## types are file, ext:memcached, ext:database, and memory (default).
41 #beaker.session.type = file
41 #beaker.session.type = file
42 #beaker.session.data_dir = %(here)s/data/sessions/data
42 #beaker.session.data_dir = %(here)s/data/sessions/data
43
43
44
44
45 The `table_name` will be automatically created on specified database if it isn't yet existing.
45 The `table_name` will be automatically created on specified database if it isn't yet existing.
46 Database specified in the `beaker.session.sa.url` can be the same that RhodeCode
46 Database specified in the `beaker.session.sa.url` can be the same that RhodeCode
47 uses, or if required it can be a different one. We recommend to use the same database.
47 uses, or if required it can be a different one. We recommend to use the same database.
48
48
49
49
50
50
51 To switch to reds-based user sessions uncomment the following section in
51 To switch to redis-based user sessions uncomment the following section in
52 your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
52 your :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini` file.
53
53
54 .. code-block:: ini
54 .. code-block:: ini
55
55
56 ## redis sessions
56 ## redis sessions
57 beaker.session.type = ext:redis
57 beaker.session.type = ext:redis
58 beaker.session.url = localhost:6379
58 beaker.session.url = localhost:6379
59
59
60
60
61 and make sure you comment out the file based sessions.
61 and make sure you comment out the file based sessions.
62
62
63 .. code-block:: ini
63 .. code-block:: ini
64
64
65 ## types are file, ext:memcached, ext:database, and memory (default).
65 ## types are file, ext:memcached, ext:database, and memory (default).
66 #beaker.session.type = file
66 #beaker.session.type = file
67 #beaker.session.data_dir = %(here)s/data/sessions/data No newline at end of file
67 #beaker.session.data_dir = %(here)s/data/sessions/data
@@ -1,113 +1,122 b''
1 .. _quick-start:
1 .. _quick-start:
2
2
3 Quick Start Installation Guide
3 Quick Start Installation Guide
4 ==============================
4 ==============================
5
5
6 .. important::
6 .. important::
7
7
8 These are quick start instructions. To optimize your |RCE|,
8 These are quick start instructions. To optimize your |RCE|,
9 |RCC|, and |RCT| usage, read the more detailed instructions in our guides.
9 |RCC|, and |RCT| usage, read the more detailed instructions in our guides.
10 For detailed installation instructions, see
10 For detailed installation instructions, see
11 :ref:`RhodeCode Control Documentation <control:rcc>`
11 :ref:`RhodeCode Control Documentation <control:rcc>`
12
12
13 .. tip::
13 .. tip::
14
14
15 If using a non-SQLite database, install and configure the database, create
15 If using a non-SQLite database, install and configure the database, create
16 a new user, and grant permissions. You will be prompted for this user's
16 a new user, and grant permissions. You will be prompted for this user's
17 credentials during |RCE| installation. See the relevant database
17 credentials during |RCE| installation. See the relevant database
18 documentation for more details.
18 documentation for more details.
19
19
20 To get |RCE| up and running, run through the below steps:
20 To get |RCE| up and running, run through the below steps:
21
21
22 1. Download the latest |RCC| installer from `rhodecode.com/download`_.
22 1. Download the latest |RCC| installer from `rhodecode.com/download`_.
23 If you don't have an account, sign up at `rhodecode.com/register`_.
23 If you don't have an account, sign up at `rhodecode.com/register`_.
24
24
25 2. Run the |RCC| installer and accept the End User Licence using the
25 2. Run the |RCC| installer and accept the End User Licence using the
26 following example:
26 following example:
27
27
28 .. code-block:: bash
28 .. code-block:: bash
29
29
30 $ chmod +x RhodeCode-installer-linux-*
30 $ chmod +x RhodeCode-installer-linux-*
31 $ ./RhodeCode-installer-linux-*
31 $ ./RhodeCode-installer-linux-*
32
32
33 Do you accept the RhodeCode Control license?
33 Do you accept the RhodeCode Control license?
34 Press [Y] to accept license and [V] to view license text: y
34 Press [Y] to accept license and [V] to view license text: y
35
35
36
37 .. important::
38
39 We recommend running RhodeCode as a non-root user, such as `rhodecode`;
40 this user must have a proper home directory.
41 Either log in as that user to install the software, or do it as root
42 with `sudo -i -u rhodecode ./RhodeCode-installer-linux-*`
43
44
36 3. Install a VCS Server, and configure it to start at boot.
45 3. Install a VCS Server, and configure it to start at boot.
37
46
38 .. code-block:: bash
47 .. code-block:: bash
39
48
40 $ rccontrol install VCSServer
49 $ rccontrol install VCSServer
41
50
42 Agree to the licence agreement? [y/N]: y
51 Agree to the licence agreement? [y/N]: y
43 IP to start the server on [127.0.0.1]:
52 IP to start the server on [127.0.0.1]:
44 Port for the server to start [10005]:
53 Port for the server to start [10005]:
45 Creating new instance: vcsserver-1
54 Creating new instance: vcsserver-1
46 Installing RhodeCode VCSServer
55 Installing RhodeCode VCSServer
47 Configuring RhodeCode VCS Server ...
56 Configuring RhodeCode VCS Server ...
48 Supervisord state is: RUNNING
57 Supervisord state is: RUNNING
49 Added process group vcsserver-1
58 Added process group vcsserver-1
50
59
51
60
52 4. Install |RCEE| or |RCCE|. If using MySQL or PostgreSQL, during
61 4. Install |RCEE| or |RCCE|. If using MySQL or PostgreSQL, during
53 installation you'll be asked for your database credentials, so have them at hand.
62 installation you'll be asked for your database credentials, so have them at hand.
54 Mysql or Postgres needs to be running and a new database needs to be created.
63 Mysql or Postgres needs to be running and a new database needs to be created.
55 You don't need any credentials or to create a database for SQLite.
64 You don't need any credentials or to create a database for SQLite.
56
65
57 .. code-block:: bash
66 .. code-block:: bash
58 :emphasize-lines: 11-16
67 :emphasize-lines: 11-16
59
68
60 $ rccontrol install Community
69 $ rccontrol install Community
61
70
62 or
71 or
63
72
64 $ rccontrol install Enterprise
73 $ rccontrol install Enterprise
65
74
66 Username [admin]: username
75 Username [admin]: username
67 Password (min 6 chars):
76 Password (min 6 chars):
68 Repeat for confirmation:
77 Repeat for confirmation:
69 Email: your@mail.com
78 Email: your@mail.com
70 Respositories location [/home/brian/repos]:
79 Respositories location [/home/brian/repos]:
71 IP to start the Enterprise server on [127.0.0.1]:
80 IP to start the Enterprise server on [127.0.0.1]:
72 Port for the Enterprise server to use [10004]:
81 Port for the Enterprise server to use [10004]:
73 Database type - [s]qlite, [m]ysql, [p]ostresql:
82 Database type - [s]qlite, [m]ysql, [p]ostresql:
74 PostgreSQL selected
83 PostgreSQL selected
75 Database host [127.0.0.1]:
84 Database host [127.0.0.1]:
76 Database port [5432]:
85 Database port [5432]:
77 Database username: db-user-name
86 Database username: db-user-name
78 Database password: somepassword
87 Database password: somepassword
79 Database name: example-db-name
88 Database name: example-db-name
80
89
81 5. Check the status of your installation. You |RCEE|/|RCCE| instance runs
90 5. Check the status of your installation. You |RCEE|/|RCCE| instance runs
82 on the URL displayed in the status message.
91 on the URL displayed in the status message.
83
92
84 .. code-block:: bash
93 .. code-block:: bash
85
94
86 $ rccontrol status
95 $ rccontrol status
87
96
88 - NAME: enterprise-1
97 - NAME: enterprise-1
89 - STATUS: RUNNING
98 - STATUS: RUNNING
90 - TYPE: Enterprise
99 - TYPE: Enterprise
91 - VERSION: 4.1.0
100 - VERSION: 4.1.0
92 - URL: http://127.0.0.1:10003
101 - URL: http://127.0.0.1:10003
93
102
94 - NAME: vcsserver-1
103 - NAME: vcsserver-1
95 - STATUS: RUNNING
104 - STATUS: RUNNING
96 - TYPE: VCSServer
105 - TYPE: VCSServer
97 - VERSION: 4.1.0
106 - VERSION: 4.1.0
98 - URL: http://127.0.0.1:10001
107 - URL: http://127.0.0.1:10001
99
108
100 .. note::
109 .. note::
101
110
102 Recommended post quick start install instructions:
111 Recommended post quick start install instructions:
103
112
104 * Read the documentation
113 * Read the documentation
105 * Carry out the :ref:`rhodecode-post-instal-ref`
114 * Carry out the :ref:`rhodecode-post-instal-ref`
106 * Set up :ref:`indexing-ref`
115 * Set up :ref:`indexing-ref`
107 * Familiarise yourself with the :ref:`rhodecode-admin-ref` section.
116 * Familiarise yourself with the :ref:`rhodecode-admin-ref` section.
108
117
109 .. _rhodecode.com/download/: https://rhodecode.com/download/
118 .. _rhodecode.com/download/: https://rhodecode.com/download/
110 .. _rhodecode.com: https://rhodecode.com/
119 .. _rhodecode.com: https://rhodecode.com/
111 .. _rhodecode.com/register: https://rhodecode.com/register/
120 .. _rhodecode.com/register: https://rhodecode.com/register/
112 .. _rhodecode.com/download: https://rhodecode.com/download/
121 .. _rhodecode.com/download: https://rhodecode.com/download/
113
122
@@ -1,230 +1,239 b''
1 |RCE| 4.18.0 |RNS|
1 |RCE| 4.18.0 |RNS|
2 ------------------
2 ------------------
3
3
4 Release Date
4 Release Date
5 ^^^^^^^^^^^^
5 ^^^^^^^^^^^^
6
6
7 - 2020-01-05
7 - 2020-01-05
8
8
9
9
10 New Features
10 New Features
11 ^^^^^^^^^^^^
11 ^^^^^^^^^^^^
12
12
13 - Artifacts: are no longer in BETA. New info page is available for uploaded artifacts
13 - Artifacts: are no longer in BETA. New info page is available for uploaded artifacts
14 which exposes some useful information like sha256, various access urls etc, and also
14 which exposes some useful information like sha256, various access urls etc, and also
15 allows deletion of artifacts, and updating their description.
15 allows deletion of artifacts, and updating their description.
16 - Artifacts: support new download url based on access to artifacts using new auth-token types.
16 - Artifacts: support new download url based on access to artifacts using new auth-token types.
17 - Artifacts: added ability to store artifacts using API, and internal cli upload.
17 - Artifacts: added ability to store artifacts using API, and internal cli upload.
18 This allows uploading of artifacts that can have 100s of GBs in size efficiently.
18 This allows uploading of artifacts that can have 100s of GBs in size efficiently.
19 - Artifacts: added metadata logic to store various extra custom data for artifacts.
19 - Artifacts: added metadata logic to store various extra custom data for artifacts.
20 - Comments: added support for adding comment attachments using the artifacts logic.
20 - Comments: added support for adding comment attachments using the artifacts logic.
21 Logged in users can now pick or drag and drop attachments into comment forms.
21 Logged in users can now pick or drag and drop attachments into comment forms.
22 - Comments: enable linkification of certain patterns on comments in repo/pull request scopes.
22 - Comments: enable linkification of certain patterns on comments in repo/pull request scopes.
23 This will render now active links to commits, pull-requests mentioned in comments body.
23 This will render now active links to commits, pull-requests mentioned in comments body.
24 - Jira: new update integration plugin.
24 - Jira: new update integration plugin.
25 Plugin now fetches possible transitions from tickets and show them to users in the interface.
25 Plugin now fetches possible transitions from tickets and show them to users in the interface.
26 Allow sending extra attributes during a transition like `resolution` message.
26 Allow sending extra attributes during a transition like `resolution` message.
27 - Navigation: Added new consistent and contextual way of creating new objects
27 - Navigation: Added new consistent and contextual way of creating new objects
28 likes gists, repositories, and repository groups using dedicated action (with a `+` sign)
28 likes gists, repositories, and repository groups using dedicated action (with a `+` sign)
29 available in the top navigation.
29 available in the top navigation.
30 - Hovercards: added new tooltips and hovercards to expose certain information for objects shown in UI.
30 - Hovercards: added new tooltips and hovercards to expose certain information for objects shown in UI.
31 RhodeCode usernames, issues, pull-requests will have active hovercard logic that will
31 RhodeCode usernames, issues, pull-requests will have active hovercard logic that will
32 load extra information about them and exposing them to users.
32 load extra information about them and exposing them to users.
33 - Files: all readme files found in repository file browser will be now rendered, allowing having readme per directory.
33 - Files: all readme files found in repository file browser will be now rendered, allowing having readme per directory.
34 - Search: expose line counts in search files information.
34 - Search: expose line counts in search files information.
35 - Audit-logs: expose download user audit logs as JSON file.
35 - Audit-logs: expose download user audit logs as JSON file.
36 - Users: added description field for users.
36 - Users: added description field for users.
37 Allows users to write a short BIO, or description of their role in the organization.
37 Allows users to write a short BIO, or description of their role in the organization.
38 - Users: allow super-admins to change bound authentication type for users.
38 - Users: allow super-admins to change bound authentication type for users.
39 E.g internal rhodecode accounts can be changed to ldap easily from user settings page.
39 E.g internal rhodecode accounts can be changed to ldap easily from user settings page.
40 - Pull requests: simplified the UI for display view, hide less important information and expose the most important ones.
40 - Pull requests: simplified the UI for display view, hide less important information and expose the most important ones.
41 - Pull requests: add merge check that detects WIP marker in title.
41 - Pull requests: add merge check that detects WIP marker in title.
42 Usually WIP in title means unfinished task that needs still some work, such marker will prevent accidental merges.
42 Usually WIP in title means unfinished task that needs still some work, such marker will prevent accidental merges.
43 - Pull requests: TODO comments have now a dedicated box below reviewers to keep track
43 - Pull requests: TODO comments have now a dedicated box below reviewers to keep track
44 of important TODOs that still need attention before review process is finalized.
44 of important TODOs that still need attention before review process is finalized.
45 - Pull requests: participants of pull request will receive an email about update of a
45 - Pull requests: participants of pull request will receive an email about update of a
46 pull requests with a small summary of changes made.
46 pull requests with a small summary of changes made.
47 - Pull requests: change the naming from #NUM into !NUM.
47 - Pull requests: change the naming from #NUM into !NUM.
48 !NUM format is now parsed and linkified in comments and commit messages.
48 !NUM format is now parsed and linkified in comments and commit messages.
49 - Pull requests: pull requests which state is changing can now be viewed with a limited view.
49 - Pull requests: pull requests which state is changing can now be viewed with a limited view.
50 - Pull requests: re-organize merge/close buttons and merge checks according to the new UI.
50 - Pull requests: re-organize merge/close buttons and merge checks according to the new UI.
51 - Pull requests: update commits button allows a force-refresh update now using dropdown option.
51 - Pull requests: update commits button allows a force-refresh update now using dropdown option.
52 - Pull requests: added quick filter to grid view to filter/search pull requests in a repository.
52 - Pull requests: added quick filter to grid view to filter/search pull requests in a repository.
53 - Pull requests: closing a pull-request without a merge requires additional confirmation now.
53 - Pull requests: closing a pull-request without a merge requires additional confirmation now.
54 - Pull requests: merge checks will now show which files caused conflicts and are blocking the merge.
54 - Pull requests: merge checks will now show which files caused conflicts and are blocking the merge.
55 - Emails: updated all generated emails design and cleanup the data fields they expose.
55 - Emails: updated all generated emails design and cleanup the data fields they expose.
56 a) More consistent UI for all types of emails. b) Improved formatting of plaintext emails
56 a) More consistent UI for all types of emails. b) Improved formatting of plaintext emails
57 c) Added reply link to comment type emails for quicker response action.
57 c) Added reply link to comment type emails for quicker response action.
58
58
59
59
60 General
60 General
61 ^^^^^^^
61 ^^^^^^^
62
62
63 - Artifacts: don't show hidden artifacts, allow showing them via a GET ?hidden=1 flag.
63 - Artifacts: don't show hidden artifacts, allow showing them via a GET ?hidden=1 flag.
64 Hidden artifacts are for example comment attachments.
64 Hidden artifacts are for example comment attachments.
65 - UI: new commits page, according to the new design, which started on 4.17.X release lines
65 - UI: new commits page, according to the new design, which started on 4.17.X release lines
66 - UI: use explicit named actions like "create user" instead of generic "save" which is bad UX.
66 - UI: use explicit named actions like "create user" instead of generic "save" which is bad UX.
67 - UI: fixed problems with generating last change in repository groups.
67 - UI: fixed problems with generating last change in repository groups.
68 There's now a new logic that checks all objects inside group for latest update time.
68 There's now a new logic that checks all objects inside group for latest update time.
69 - API: add artifact `get_info`, and `store_metadata` methods.
69 - API: add artifact `get_info`, and `store_metadata` methods.
70 - API: allowed to specify extra recipients for pr/commit comments api methods.
70 - API: allowed to specify extra recipients for pr/commit comments api methods.
71 - Vcsserver: set file based cache as default for vcsserver which can be shared
71 - Vcsserver: set file based cache as default for vcsserver which can be shared
72 across multiple workers saving memory usage.
72 across multiple workers saving memory usage.
73 - Vcsserver: added redis as possible cache backend for even greater performance.
73 - Vcsserver: added redis as possible cache backend for even greater performance.
74 - Dependencies: bumped GIT version to 2.23.0
74 - Dependencies: bumped GIT version to 2.23.0
75 - Dependencies: bumped SVN version to 1.12.2
75 - Dependencies: bumped SVN version to 1.12.2
76 - Dependencies: bumped Mercurial version to 5.1.1 and hg-evolve to 9.1.0
76 - Dependencies: bumped Mercurial version to 5.1.1 and hg-evolve to 9.1.0
77 - Search: added logic for sorting ElasticSearch6 backend search results.
77 - Search: added logic for sorting ElasticSearch6 backend search results.
78 - User bookmarks: make it easier to re-organize existing entries.
78 - User bookmarks: make it easier to re-organize existing entries.
79 - Data grids: hide pagination for single pages in grids.
79 - Data grids: hide pagination for single pages in grids.
80 - Gists: UX, removed private/public gist buttons and replaced them with radio group.
80 - Gists: UX, removed private/public gist buttons and replaced them with radio group.
81 - Gunicorn: moved all configuration of gunicorn workers to .ini files.
81 - Gunicorn: moved all configuration of gunicorn workers to .ini files.
82 - Gunicorn: added worker memory management allowing setting maximum per-worker memory usage.
82 - Gunicorn: added worker memory management allowing setting maximum per-worker memory usage.
83 - Automation: moved update groups task into celery task
83 - Automation: moved update groups task into celery task
84 - Cache commits: add option to refresh caches manually from advanced pages.
84 - Cache commits: add option to refresh caches manually from advanced pages.
85 - Pull requests: add indication of state change in list of pull-requests and actually show them in the list.
85 - Pull requests: add indication of state change in list of pull-requests and actually show them in the list.
86 - Cache keys: register and self cleanup cache keys used for invalidation to prevent leaking lot of them into DB on worker recycle
86 - Cache keys: register and self cleanup cache keys used for invalidation to prevent leaking lot of them into DB on worker recycle
87 - Repo groups: removed locking inheritance flag from repo-groups. We'll deprecate this soon and this only brings in confusion
87 - Repo groups: removed locking inheritance flag from repo-groups. We'll deprecate this soon and this only brings in confusion
88 - System snapshot: improved formatting for better readability
88 - System snapshot: improved formatting for better readability
89 - System info: expose data about vcsserver.
89 - System info: expose data about vcsserver.
90 - Packages: updated celery to 4.3.0 and switch default backend to redis instead of RabbitMQ.
90 - Packages: updated celery to 4.3.0 and switch default backend to redis instead of RabbitMQ.
91 Redis is stable enough and easier to install. Having Redis simplifies the stack as it's used in other parts of RhodeCode.
91 Redis is stable enough and easier to install. Having Redis simplifies the stack as it's used in other parts of RhodeCode.
92 - Dependencies: bumped alembic to 1.2.1
92 - Dependencies: bumped alembic to 1.2.1
93 - Dependencies: bumped amqp==2.5.2 and kombu==4.6.6
93 - Dependencies: bumped amqp==2.5.2 and kombu==4.6.6
94 - Dependencies: bumped atomicwrites==1.3.0
94 - Dependencies: bumped atomicwrites==1.3.0
95 - Dependencies: bumped cffi==1.12.3
95 - Dependencies: bumped cffi==1.12.3
96 - Dependencies: bumped configparser==4.0.2
96 - Dependencies: bumped configparser==4.0.2
97 - Dependencies: bumped deform==2.0.8
97 - Dependencies: bumped deform==2.0.8
98 - Dependencies: bumped dogpile.cache==0.9.0
98 - Dependencies: bumped dogpile.cache==0.9.0
99 - Dependencies: bumped hupper==1.8.1
99 - Dependencies: bumped hupper==1.8.1
100 - Dependencies: bumped mako to 1.1.0
100 - Dependencies: bumped mako to 1.1.0
101 - Dependencies: bumped markupsafe to 1.1.1
101 - Dependencies: bumped markupsafe to 1.1.1
102 - Dependencies: bumped packaging==19.2
102 - Dependencies: bumped packaging==19.2
103 - Dependencies: bumped paste==3.2.1
103 - Dependencies: bumped paste==3.2.1
104 - Dependencies: bumped pastescript==3.2.0
104 - Dependencies: bumped pastescript==3.2.0
105 - Dependencies: bumped pathlib2 to 2.3.4
105 - Dependencies: bumped pathlib2 to 2.3.4
106 - Dependencies: bumped pluggy==0.13.0
106 - Dependencies: bumped pluggy==0.13.0
107 - Dependencies: bumped psutil to 5.6.3
107 - Dependencies: bumped psutil to 5.6.3
108 - Dependencies: bumped psutil==5.6.5
108 - Dependencies: bumped psutil==5.6.5
109 - Dependencies: bumped psycopg2==2.8.4
109 - Dependencies: bumped psycopg2==2.8.4
110 - Dependencies: bumped pycurl to 7.43.0.3
110 - Dependencies: bumped pycurl to 7.43.0.3
111 - Dependencies: bumped pyotp==2.3.0
111 - Dependencies: bumped pyotp==2.3.0
112 - Dependencies: bumped pyparsing to 2.4.2
112 - Dependencies: bumped pyparsing to 2.4.2
113 - Dependencies: bumped pyramid-debugtoolbar==4.5.1
113 - Dependencies: bumped pyramid-debugtoolbar==4.5.1
114 - Dependencies: bumped pyramid-mako to 1.1.0
114 - Dependencies: bumped pyramid-mako to 1.1.0
115 - Dependencies: bumped redis to 3.3.8
115 - Dependencies: bumped redis to 3.3.8
116 - Dependencies: bumped sqlalchemy to 1.3.8
116 - Dependencies: bumped sqlalchemy to 1.3.8
117 - Dependencies: bumped sqlalchemy==1.3.11
117 - Dependencies: bumped sqlalchemy==1.3.11
118 - Dependencies: bumped test libraries.
118 - Dependencies: bumped test libraries.
119 - Dependencies: freeze alembic==1.3.1
119 - Dependencies: freeze alembic==1.3.1
120 - Dependencies: freeze python-dateutil
120 - Dependencies: freeze python-dateutil
121 - Dependencies: freeze redis==3.3.11
121 - Dependencies: freeze redis==3.3.11
122 - Dependencies: freeze supervisor==4.1.0
122 - Dependencies: freeze supervisor==4.1.0
123
123
124
124
125 Security
125 Security
126 ^^^^^^^^
126 ^^^^^^^^
127
127
128 - Security: fixed issues with exposing wrong http status (403) indicating repository with
128 - Security: fixed issues with exposing wrong http status (403) indicating repository with
129 given name exists and we don't have permissions to it. This was exposed in the redirection
129 given name exists and we don't have permissions to it. This was exposed in the redirection
130 logic of the global pull-request page. In case of redirection we also exposed
130 logic of the global pull-request page. In case of redirection we also exposed
131 repository name in the URL.
131 repository name in the URL.
132
132
133
133
134 Performance
134 Performance
135 ^^^^^^^^^^^
135 ^^^^^^^^^^^
136
136
137 - Core: many various small improvements and optimizations to make rhodecode faster then before.
137 - Core: many various small improvements and optimizations to make rhodecode faster then before.
138 - VCSServer: new cache implementation for remote functions.
138 - VCSServer: new cache implementation for remote functions.
139 Single worker shared caches that can use redis/file-cache.
139 Single worker shared caches that can use redis/file-cache.
140 This greatly improves performance on larger instances, and doesn't trigger cache
140 This greatly improves performance on larger instances, and doesn't trigger cache
141 re-calculation on worker restarts.
141 re-calculation on worker restarts.
142 - GIT: switched internal git operations from Dulwich to libgit2 in order to obtain better performance and scalability.
142 - GIT: switched internal git operations from Dulwich to libgit2 in order to obtain better performance and scalability.
143 - SSH: skip loading unneeded application parts for SSH to make execution of ssh commands faster.
143 - SSH: skip loading unneeded application parts for SSH to make execution of ssh commands faster.
144 - Main page: main page will now load repositories and repositories groups using partial DB calls instead of big JSON files.
144 - Main page: main page will now load repositories and repositories groups using partial DB calls instead of big JSON files.
145 In case of many repositories in root this could lead to very slow page rendering.
145 In case of many repositories in root this could lead to very slow page rendering.
146 - Admin pages: made all grids use same DB based partial loading logic. We'll no longer fetch
146 - Admin pages: made all grids use same DB based partial loading logic. We'll no longer fetch
147 all objects into JSON for display purposes. This significantly improves speed of those pages in case
147 all objects into JSON for display purposes. This significantly improves speed of those pages in case
148 of many objects shown in them.
148 of many objects shown in them.
149 - Summary page: use non-memory cache for readme, and cleanup cache for repo stats.
149 - Summary page: use non-memory cache for readme, and cleanup cache for repo stats.
150 This change won't re-cache after worker restarts and can be shared across all workers
150 This change won't re-cache after worker restarts and can be shared across all workers
151 - Files: only check for git_lfs/hg_largefiles if they are enabled.
151 - Files: only check for git_lfs/hg_largefiles if they are enabled.
152 This speeds up fetching of files if they are not LF and very big.
152 This speeds up fetching of files if they are not LF and very big.
153 - Vcsserver: added support for streaming data from the remote methods. This allows
153 - Vcsserver: added support for streaming data from the remote methods. This allows
154 to stream very large files without taking up memory, mostly for usage in SVN when
154 to stream very large files without taking up memory, mostly for usage in SVN when
155 downloading large binaries from vcs system.
155 downloading large binaries from vcs system.
156 - Files: added streaming remote attributes for vcsserver.
156 - Files: added streaming remote attributes for vcsserver.
157 This change enables streaming raw content or raw downloads of large files without
157 This change enables streaming raw content or raw downloads of large files without
158 transferring them over to enterprise for pack & repack using msgpack.
158 transferring them over to enterprise for pack & repack using msgpack.
159 Msgpack has a limit of 2gb and generally pack+repack for ~2gb is very slow.
159 Msgpack has a limit of 2gb and generally pack+repack for ~2gb is very slow.
160 - Files: ensure over size limit files never do any content fetching when viewing such files.
160 - Files: ensure over size limit files never do any content fetching when viewing such files.
161 - VCSServer: skip host verification to speed up pycurl calls.
161 - VCSServer: skip host verification to speed up pycurl calls.
162 - User-bookmarks: cache fetching of bookmarks since this is quite expensive query to
162 - User-bookmarks: cache fetching of bookmarks since this is quite expensive query to
163 make with joinedload on repos/repo groups.
163 make with joinedload on repos/repo groups.
164 - Goto-switcher: reduce query data to only required attributes for speedups.
164 - Goto-switcher: reduce query data to only required attributes for speedups.
165 - My account: owner/watched repos are now loaded only using DB queries.
165 - My account: owner/watched repos are now loaded only using DB queries.
166
166
167
167
168 Fixes
168 Fixes
169 ^^^^^
169 ^^^^^
170
170
171 - Mercurial: move imports from top-level to prevent from loading mercurial code on hook execution for svn/git.
171 - Mercurial: move imports from top-level to prevent from loading mercurial code on hook execution for svn/git.
172 - GIT: limit sync-fetch logic to only retrieve tags/ and heads/ with default execution arguments.
172 - GIT: limit sync-fetch logic to only retrieve tags/ and heads/ with default execution arguments.
173 - GIT: fixed issue with git submodules detection.
173 - GIT: fixed issue with git submodules detection.
174 - SVN: fix checkout url for ssh+svn backend not having special prefix resulting in incorrect command shown.
174 - SVN: fix checkout url for ssh+svn backend not having special prefix resulting in incorrect command shown.
175 - SVN: fixed problem with showing empty directories.
175 - SVN: fixed problem with showing empty directories.
176 - OAuth: use a vendored version of `authomatic` library, and switch Bitbucket authentication to use oauth2.
176 - OAuth: use a vendored version of `authomatic` library, and switch Bitbucket authentication to use oauth2.
177 - Diffs: handle paths with quotes in diffs.
177 - Diffs: handle paths with quotes in diffs.
178 - Diffs: fixed outdated files in pull-requests re-using the filediff raw_id for anchor generation. Fixes #5567
178 - Diffs: fixed outdated files in pull-requests re-using the filediff raw_id for anchor generation. Fixes #5567
179 - Diffs: toggle race condition on sticky vs wide-diff-mode that caused some display problems on larger diffs.
179 - Diffs: toggle race condition on sticky vs wide-diff-mode that caused some display problems on larger diffs.
180 - Pull requests: handle exceptions in state change and improve logging.
180 - Pull requests: handle exceptions in state change and improve logging.
181 - Pull requests: fixed title/description generation for single commits which are numbers.
181 - Pull requests: fixed title/description generation for single commits which are numbers.
182 - Pull requests: changed the source of changes to be using shadow repos if it exists.
182 - Pull requests: changed the source of changes to be using shadow repos if it exists.
183 In case of `git push -f` and rebase we lost commits in the repo resulting in
183 In case of `git push -f` and rebase we lost commits in the repo resulting in
184 problems of displaying versions of pull-requests.
184 problems of displaying versions of pull-requests.
185 - Pull requests: handle case when removing existing files from a repository in compare versions diff.
185 - Pull requests: handle case when removing existing files from a repository in compare versions diff.
186 - Files: don't expose copy content helper in case of binary files.
186 - Files: don't expose copy content helper in case of binary files.
187 - Registration: properly expose first_name/last_name into email on user registration.
187 - Registration: properly expose first_name/last_name into email on user registration.
188 - Markup renderers: fixed broken code highlight for rst files.
188 - Markup renderers: fixed broken code highlight for rst files.
189 - Ui: make super admin be named consistently across ui.
189 - Ui: make super admin be named consistently across ui.
190 - Audit logs: fixed search cases with special chars such as `-`.
190 - Audit logs: fixed search cases with special chars such as `-`.
191
191
192
192
193 Upgrade notes
193 Upgrade notes
194 ^^^^^^^^^^^^^
194 ^^^^^^^^^^^^^
195
195
196 - Major Celery Version upgrade. The 4.18.X release includes a major Celery version.
197 It's recommended to run `rccontrol self-stop && rccontrol self-init` after the
198 upgrade to ensure celery workers are restarted and updated.
199
196 - New Automation task. We've changed the logic for updating latest change inside repository group.
200 - New Automation task. We've changed the logic for updating latest change inside repository group.
197 New logic includes scanning for changes in all nested objects. Since this is a heavy task
201 New logic includes scanning for changes in all nested objects. Since this is a heavy task
198 a new dedicated scheduler task has been created to update it automatically on a scheduled base.
202 a new dedicated scheduler task has been created to update it automatically on a scheduled base.
199 Please review in `admin > settings > automation` to enable this task.
203 Please review in `admin > settings > automation` to enable this task.
200
204
201 - New safer encryption algorithm. Some setting values are encrypted before storing it inside the database.
205 - New safer encryption algorithm. Some setting values are encrypted before storing it inside the database.
202 To keep full backward compatibility old AES algorithm is used.
206 To keep full backward compatibility old AES algorithm is used.
203 If you wish to enable a safer option set fernet encryption instead inside rhodecode.ini
207 If you wish to enable a safer option set fernet encryption instead inside rhodecode.ini
204 `rhodecode.encrypted_values.algorithm = fernet`
208 `rhodecode.encrypted_values.algorithm = fernet`
205
209
206 - Pull requests UI changes. We've simplified the UI on pull requests page.
210 - Pull requests UI changes. We've simplified the UI on pull requests page.
207 Please review the new UI to prevent surprises. All actions from old UI should be still possible with the new one.
211 Please review the new UI to prevent surprises. All actions from old UI should be still possible with the new one.
208
212
209 - Redis is now a default recommended backend for Celery and replaces previous rabbitmq.
213 - Redis is now a default recommended backend for Celery and replaces previous rabbitmq.
210 Redis is generally easier to manage and install, and it's also very stable for usage
214 Redis is generally easier to manage and install, and it's also very stable for usage
211 in the scheduler/celery async tasks. Since we also recommend Redis for caches the application
215 in the scheduler/celery async tasks. Since we also recommend Redis for caches the application
212 stack can be simplified by removing rabbitmq and replacing it with single Redis instance.
216 stack can be simplified by removing rabbitmq and replacing it with single Redis instance.
213
217
214 - Recommendation for using Redis as the new cache backend on vcsserver.
218 - Recommendation for using Redis as the new cache backend on vcsserver.
215 Since Version 4.18.0 VCSServer has a new cache implementation for VCS data.
219 Since Version 4.18.0 VCSServer has a new cache implementation for VCS data.
216 By default, for simplicity the cache type is file based. We strongly recommend using
220 By default, for simplicity the cache type is file based. We strongly recommend using
217 Redis instead for better Performance and scalability
221 Redis instead for better Performance and scalability
218 Please review vcsserver.ini settings under:
222 Please review vcsserver.ini settings under:
219 `rc_cache.repo_object.backend = dogpile.cache.rc.redis_msgpack`
223 `rc_cache.repo_object.backend = dogpile.cache.rc.redis_msgpack`
220
224
225 - Gunicorn configuration now moved to .ini files.
226 Upgrading to 4.18.X will overwrite the gunicorn_conf.py file. If there are any custom changes in that file
227 they will be lost. Recommended way to configure gunicorn is now via the .ini files. Please check `rhodecode.template.ini` file
228 for example gunicorn configuration.
229
221 - New memory monitoring for Gunicorn workers. Starting from 4.18 release a option was added
230 - New memory monitoring for Gunicorn workers. Starting from 4.18 release a option was added
222 to limit the maximum amount of memory used by a worker.
231 to limit the maximum amount of memory used by a worker.
223 Please review new settings in `[server:main]` section for memory management in both
232 Please review new settings in `[server:main]` section for memory management in both
224 rhodecode.ini and vcsserver.ini::
233 rhodecode.ini and vcsserver.ini::
225
234
226 ; Maximum memory usage that each worker can use before it will receive a
235 ; Maximum memory usage that each worker can use before it will receive a
227 ; graceful restart signal 0 = memory monitoring is disabled
236 ; graceful restart signal 0 = memory monitoring is disabled
228 ; Examples: 268435456 (256MB), 536870912 (512MB)
237 ; Examples: 268435456 (256MB), 536870912 (512MB)
229 ; 1073741824 (1GB), 2147483648 (2GB), 4294967296 (4GB)
238 ; 1073741824 (1GB), 2147483648 (2GB), 4294967296 (4GB)
230 memory_max_usage = 0
239 memory_max_usage = 0
@@ -1,137 +1,140 b''
1 .. _rhodecode-release-notes-ref:
1 .. _rhodecode-release-notes-ref:
2
2
3 Release Notes
3 Release Notes
4 =============
4 =============
5
5
6 |RCE| 4.x Versions
6 |RCE| 4.x Versions
7 ------------------
7 ------------------
8
8
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.18.3.rst
13 release-notes-4.18.2.rst
14 release-notes-4.18.1.rst
12 release-notes-4.18.0.rst
15 release-notes-4.18.0.rst
13 release-notes-4.17.4.rst
16 release-notes-4.17.4.rst
14 release-notes-4.17.3.rst
17 release-notes-4.17.3.rst
15 release-notes-4.17.2.rst
18 release-notes-4.17.2.rst
16 release-notes-4.17.1.rst
19 release-notes-4.17.1.rst
17 release-notes-4.17.0.rst
20 release-notes-4.17.0.rst
18 release-notes-4.16.2.rst
21 release-notes-4.16.2.rst
19 release-notes-4.16.1.rst
22 release-notes-4.16.1.rst
20 release-notes-4.16.0.rst
23 release-notes-4.16.0.rst
21 release-notes-4.15.2.rst
24 release-notes-4.15.2.rst
22 release-notes-4.15.1.rst
25 release-notes-4.15.1.rst
23 release-notes-4.15.0.rst
26 release-notes-4.15.0.rst
24 release-notes-4.14.1.rst
27 release-notes-4.14.1.rst
25 release-notes-4.14.0.rst
28 release-notes-4.14.0.rst
26 release-notes-4.13.3.rst
29 release-notes-4.13.3.rst
27 release-notes-4.13.2.rst
30 release-notes-4.13.2.rst
28 release-notes-4.13.1.rst
31 release-notes-4.13.1.rst
29 release-notes-4.13.0.rst
32 release-notes-4.13.0.rst
30 release-notes-4.12.4.rst
33 release-notes-4.12.4.rst
31 release-notes-4.12.3.rst
34 release-notes-4.12.3.rst
32 release-notes-4.12.2.rst
35 release-notes-4.12.2.rst
33 release-notes-4.12.1.rst
36 release-notes-4.12.1.rst
34 release-notes-4.12.0.rst
37 release-notes-4.12.0.rst
35 release-notes-4.11.6.rst
38 release-notes-4.11.6.rst
36 release-notes-4.11.5.rst
39 release-notes-4.11.5.rst
37 release-notes-4.11.4.rst
40 release-notes-4.11.4.rst
38 release-notes-4.11.3.rst
41 release-notes-4.11.3.rst
39 release-notes-4.11.2.rst
42 release-notes-4.11.2.rst
40 release-notes-4.11.1.rst
43 release-notes-4.11.1.rst
41 release-notes-4.11.0.rst
44 release-notes-4.11.0.rst
42 release-notes-4.10.6.rst
45 release-notes-4.10.6.rst
43 release-notes-4.10.5.rst
46 release-notes-4.10.5.rst
44 release-notes-4.10.4.rst
47 release-notes-4.10.4.rst
45 release-notes-4.10.3.rst
48 release-notes-4.10.3.rst
46 release-notes-4.10.2.rst
49 release-notes-4.10.2.rst
47 release-notes-4.10.1.rst
50 release-notes-4.10.1.rst
48 release-notes-4.10.0.rst
51 release-notes-4.10.0.rst
49 release-notes-4.9.1.rst
52 release-notes-4.9.1.rst
50 release-notes-4.9.0.rst
53 release-notes-4.9.0.rst
51 release-notes-4.8.0.rst
54 release-notes-4.8.0.rst
52 release-notes-4.7.2.rst
55 release-notes-4.7.2.rst
53 release-notes-4.7.1.rst
56 release-notes-4.7.1.rst
54 release-notes-4.7.0.rst
57 release-notes-4.7.0.rst
55 release-notes-4.6.1.rst
58 release-notes-4.6.1.rst
56 release-notes-4.6.0.rst
59 release-notes-4.6.0.rst
57 release-notes-4.5.2.rst
60 release-notes-4.5.2.rst
58 release-notes-4.5.1.rst
61 release-notes-4.5.1.rst
59 release-notes-4.5.0.rst
62 release-notes-4.5.0.rst
60 release-notes-4.4.2.rst
63 release-notes-4.4.2.rst
61 release-notes-4.4.1.rst
64 release-notes-4.4.1.rst
62 release-notes-4.4.0.rst
65 release-notes-4.4.0.rst
63 release-notes-4.3.1.rst
66 release-notes-4.3.1.rst
64 release-notes-4.3.0.rst
67 release-notes-4.3.0.rst
65 release-notes-4.2.1.rst
68 release-notes-4.2.1.rst
66 release-notes-4.2.0.rst
69 release-notes-4.2.0.rst
67 release-notes-4.1.2.rst
70 release-notes-4.1.2.rst
68 release-notes-4.1.1.rst
71 release-notes-4.1.1.rst
69 release-notes-4.1.0.rst
72 release-notes-4.1.0.rst
70 release-notes-4.0.1.rst
73 release-notes-4.0.1.rst
71 release-notes-4.0.0.rst
74 release-notes-4.0.0.rst
72
75
73 |RCE| 3.x Versions
76 |RCE| 3.x Versions
74 ------------------
77 ------------------
75
78
76 .. toctree::
79 .. toctree::
77 :maxdepth: 1
80 :maxdepth: 1
78
81
79 release-notes-3.8.4.rst
82 release-notes-3.8.4.rst
80 release-notes-3.8.3.rst
83 release-notes-3.8.3.rst
81 release-notes-3.8.2.rst
84 release-notes-3.8.2.rst
82 release-notes-3.8.1.rst
85 release-notes-3.8.1.rst
83 release-notes-3.8.0.rst
86 release-notes-3.8.0.rst
84 release-notes-3.7.1.rst
87 release-notes-3.7.1.rst
85 release-notes-3.7.0.rst
88 release-notes-3.7.0.rst
86 release-notes-3.6.1.rst
89 release-notes-3.6.1.rst
87 release-notes-3.6.0.rst
90 release-notes-3.6.0.rst
88 release-notes-3.5.2.rst
91 release-notes-3.5.2.rst
89 release-notes-3.5.1.rst
92 release-notes-3.5.1.rst
90 release-notes-3.5.0.rst
93 release-notes-3.5.0.rst
91 release-notes-3.4.1.rst
94 release-notes-3.4.1.rst
92 release-notes-3.4.0.rst
95 release-notes-3.4.0.rst
93 release-notes-3.3.4.rst
96 release-notes-3.3.4.rst
94 release-notes-3.3.3.rst
97 release-notes-3.3.3.rst
95 release-notes-3.3.2.rst
98 release-notes-3.3.2.rst
96 release-notes-3.3.1.rst
99 release-notes-3.3.1.rst
97 release-notes-3.3.0.rst
100 release-notes-3.3.0.rst
98 release-notes-3.2.3.rst
101 release-notes-3.2.3.rst
99 release-notes-3.2.2.rst
102 release-notes-3.2.2.rst
100 release-notes-3.2.1.rst
103 release-notes-3.2.1.rst
101 release-notes-3.2.0.rst
104 release-notes-3.2.0.rst
102 release-notes-3.1.1.rst
105 release-notes-3.1.1.rst
103 release-notes-3.1.0.rst
106 release-notes-3.1.0.rst
104 release-notes-3.0.2.rst
107 release-notes-3.0.2.rst
105 release-notes-3.0.1.rst
108 release-notes-3.0.1.rst
106 release-notes-3.0.0.rst
109 release-notes-3.0.0.rst
107
110
108 |RCE| 2.x Versions
111 |RCE| 2.x Versions
109 ------------------
112 ------------------
110
113
111 .. toctree::
114 .. toctree::
112 :maxdepth: 1
115 :maxdepth: 1
113
116
114 release-notes-2.2.8.rst
117 release-notes-2.2.8.rst
115 release-notes-2.2.7.rst
118 release-notes-2.2.7.rst
116 release-notes-2.2.6.rst
119 release-notes-2.2.6.rst
117 release-notes-2.2.5.rst
120 release-notes-2.2.5.rst
118 release-notes-2.2.4.rst
121 release-notes-2.2.4.rst
119 release-notes-2.2.3.rst
122 release-notes-2.2.3.rst
120 release-notes-2.2.2.rst
123 release-notes-2.2.2.rst
121 release-notes-2.2.1.rst
124 release-notes-2.2.1.rst
122 release-notes-2.2.0.rst
125 release-notes-2.2.0.rst
123 release-notes-2.1.0.rst
126 release-notes-2.1.0.rst
124 release-notes-2.0.2.rst
127 release-notes-2.0.2.rst
125 release-notes-2.0.1.rst
128 release-notes-2.0.1.rst
126 release-notes-2.0.0.rst
129 release-notes-2.0.0.rst
127
130
128 |RCE| 1.x Versions
131 |RCE| 1.x Versions
129 ------------------
132 ------------------
130
133
131 .. toctree::
134 .. toctree::
132 :maxdepth: 1
135 :maxdepth: 1
133
136
134 release-notes-1.7.2.rst
137 release-notes-1.7.2.rst
135 release-notes-1.7.1.rst
138 release-notes-1.7.1.rst
136 release-notes-1.7.0.rst
139 release-notes-1.7.0.rst
137 release-notes-1.6.0.rst
140 release-notes-1.6.0.rst
@@ -1,554 +1,555 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
22 import itertools
21 import itertools
23 import logging
22 import logging
24 import sys
23 import sys
25 import types
24 import types
26 import fnmatch
25 import fnmatch
27
26
28 import decorator
27 import decorator
29 import venusian
28 import venusian
30 from collections import OrderedDict
29 from collections import OrderedDict
31
30
32 from pyramid.exceptions import ConfigurationError
31 from pyramid.exceptions import ConfigurationError
33 from pyramid.renderers import render
32 from pyramid.renderers import render
34 from pyramid.response import Response
33 from pyramid.response import Response
35 from pyramid.httpexceptions import HTTPNotFound
34 from pyramid.httpexceptions import HTTPNotFound
36
35
37 from rhodecode.api.exc import (
36 from rhodecode.api.exc import (
38 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
39 from rhodecode.apps._base import TemplateArgs
38 from rhodecode.apps._base import TemplateArgs
40 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
41 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
42 from rhodecode.lib.exc_tracking import store_exception
41 from rhodecode.lib.exc_tracking import store_exception
43 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.utils2 import safe_str
45 from rhodecode.lib.plugins.utils import get_plugin_settings
44 from rhodecode.lib.plugins.utils import get_plugin_settings
46 from rhodecode.model.db import User, UserApiKeys
45 from rhodecode.model.db import User, UserApiKeys
47
46
48 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
49
48
50 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 DEFAULT_RENDERER = 'jsonrpc_renderer'
51 DEFAULT_URL = '/_admin/apiv2'
50 DEFAULT_URL = '/_admin/apiv2'
52
51
53
52
54 def find_methods(jsonrpc_methods, pattern):
53 def find_methods(jsonrpc_methods, pattern):
55 matches = OrderedDict()
54 matches = OrderedDict()
56 if not isinstance(pattern, (list, tuple)):
55 if not isinstance(pattern, (list, tuple)):
57 pattern = [pattern]
56 pattern = [pattern]
58
57
59 for single_pattern in pattern:
58 for single_pattern in pattern:
60 for method_name, method in jsonrpc_methods.items():
59 for method_name, method in jsonrpc_methods.items():
61 if fnmatch.fnmatch(method_name, single_pattern):
60 if fnmatch.fnmatch(method_name, single_pattern):
62 matches[method_name] = method
61 matches[method_name] = method
63 return matches
62 return matches
64
63
65
64
66 class ExtJsonRenderer(object):
65 class ExtJsonRenderer(object):
67 """
66 """
68 Custom renderer that mkaes use of our ext_json lib
67 Custom renderer that mkaes use of our ext_json lib
69
68
70 """
69 """
71
70
72 def __init__(self, serializer=json.dumps, **kw):
71 def __init__(self, serializer=json.dumps, **kw):
73 """ Any keyword arguments will be passed to the ``serializer``
72 """ Any keyword arguments will be passed to the ``serializer``
74 function."""
73 function."""
75 self.serializer = serializer
74 self.serializer = serializer
76 self.kw = kw
75 self.kw = kw
77
76
78 def __call__(self, info):
77 def __call__(self, info):
79 """ Returns a plain JSON-encoded string with content-type
78 """ Returns a plain JSON-encoded string with content-type
80 ``application/json``. The content-type may be overridden by
79 ``application/json``. The content-type may be overridden by
81 setting ``request.response.content_type``."""
80 setting ``request.response.content_type``."""
82
81
83 def _render(value, system):
82 def _render(value, system):
84 request = system.get('request')
83 request = system.get('request')
85 if request is not None:
84 if request is not None:
86 response = request.response
85 response = request.response
87 ct = response.content_type
86 ct = response.content_type
88 if ct == response.default_content_type:
87 if ct == response.default_content_type:
89 response.content_type = 'application/json'
88 response.content_type = 'application/json'
90
89
91 return self.serializer(value, **self.kw)
90 return self.serializer(value, **self.kw)
92
91
93 return _render
92 return _render
94
93
95
94
96 def jsonrpc_response(request, result):
95 def jsonrpc_response(request, result):
97 rpc_id = getattr(request, 'rpc_id', None)
96 rpc_id = getattr(request, 'rpc_id', None)
98 response = request.response
97 response = request.response
99
98
100 # store content_type before render is called
99 # store content_type before render is called
101 ct = response.content_type
100 ct = response.content_type
102
101
103 ret_value = ''
102 ret_value = ''
104 if rpc_id:
103 if rpc_id:
105 ret_value = {
104 ret_value = {
106 'id': rpc_id,
105 'id': rpc_id,
107 'result': result,
106 'result': result,
108 'error': None,
107 'error': None,
109 }
108 }
110
109
111 # fetch deprecation warnings, and store it inside results
110 # fetch deprecation warnings, and store it inside results
112 deprecation = getattr(request, 'rpc_deprecation', None)
111 deprecation = getattr(request, 'rpc_deprecation', None)
113 if deprecation:
112 if deprecation:
114 ret_value['DEPRECATION_WARNING'] = deprecation
113 ret_value['DEPRECATION_WARNING'] = deprecation
115
114
116 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
117 response.body = safe_str(raw_body, response.charset)
116 response.body = safe_str(raw_body, response.charset)
118
117
119 if ct == response.default_content_type:
118 if ct == response.default_content_type:
120 response.content_type = 'application/json'
119 response.content_type = 'application/json'
121
120
122 return response
121 return response
123
122
124
123
125 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
126 """
125 """
127 Generate a Response object with a JSON-RPC error body
126 Generate a Response object with a JSON-RPC error body
128
127
129 :param code:
128 :param code:
130 :param retid:
129 :param retid:
131 :param message:
130 :param message:
132 """
131 """
133 err_dict = {'id': retid, 'result': None, 'error': message}
132 err_dict = {'id': retid, 'result': None, 'error': message}
134 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
135
134
136 return Response(
135 return Response(
137 body=body,
136 body=body,
138 status=code,
137 status=code,
139 content_type='application/json',
138 content_type='application/json',
140 headerlist=headers
139 headerlist=headers
141 )
140 )
142
141
143
142
144 def exception_view(exc, request):
143 def exception_view(exc, request):
145 rpc_id = getattr(request, 'rpc_id', None)
144 rpc_id = getattr(request, 'rpc_id', None)
146
145
147 if isinstance(exc, JSONRPCError):
146 if isinstance(exc, JSONRPCError):
148 fault_message = safe_str(exc.message)
147 fault_message = safe_str(exc.message)
149 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
148 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
150 elif isinstance(exc, JSONRPCValidationError):
149 elif isinstance(exc, JSONRPCValidationError):
151 colander_exc = exc.colander_exception
150 colander_exc = exc.colander_exception
152 # TODO(marcink): think maybe of nicer way to serialize errors ?
151 # TODO(marcink): think maybe of nicer way to serialize errors ?
153 fault_message = colander_exc.asdict()
152 fault_message = colander_exc.asdict()
154 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
153 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
155 elif isinstance(exc, JSONRPCForbidden):
154 elif isinstance(exc, JSONRPCForbidden):
156 fault_message = 'Access was denied to this resource.'
155 fault_message = 'Access was denied to this resource.'
157 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
156 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
158 elif isinstance(exc, HTTPNotFound):
157 elif isinstance(exc, HTTPNotFound):
159 method = request.rpc_method
158 method = request.rpc_method
160 log.debug('json-rpc method `%s` not found in list of '
159 log.debug('json-rpc method `%s` not found in list of '
161 'api calls: %s, rpc_id:%s',
160 'api calls: %s, rpc_id:%s',
162 method, request.registry.jsonrpc_methods.keys(), rpc_id)
161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
163
162
164 similar = 'none'
163 similar = 'none'
165 try:
164 try:
166 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
167 similar_found = find_methods(
166 similar_found = find_methods(
168 request.registry.jsonrpc_methods, similar_paterns)
167 request.registry.jsonrpc_methods, similar_paterns)
169 similar = ', '.join(similar_found.keys()) or similar
168 similar = ', '.join(similar_found.keys()) or similar
170 except Exception:
169 except Exception:
171 # make the whole above block safe
170 # make the whole above block safe
172 pass
171 pass
173
172
174 fault_message = "No such method: {}. Similar methods: {}".format(
173 fault_message = "No such method: {}. Similar methods: {}".format(
175 method, similar)
174 method, similar)
176 else:
175 else:
177 fault_message = 'undefined error'
176 fault_message = 'undefined error'
178 exc_info = exc.exc_info()
177 exc_info = exc.exc_info()
179 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
178 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
180
179
181 return jsonrpc_error(request, fault_message, rpc_id)
180 return jsonrpc_error(request, fault_message, rpc_id)
182
181
183
182
184 def request_view(request):
183 def request_view(request):
185 """
184 """
186 Main request handling method. It handles all logic to call a specific
185 Main request handling method. It handles all logic to call a specific
187 exposed method
186 exposed method
188 """
187 """
188 # cython compatible inspect
189 from rhodecode.config.patches import inspect_getargspec
190 inspect = inspect_getargspec()
189
191
190 # check if we can find this session using api_key, get_by_auth_token
192 # check if we can find this session using api_key, get_by_auth_token
191 # search not expired tokens only
193 # search not expired tokens only
192
193 try:
194 try:
194 api_user = User.get_by_auth_token(request.rpc_api_key)
195 api_user = User.get_by_auth_token(request.rpc_api_key)
195
196
196 if api_user is None:
197 if api_user is None:
197 return jsonrpc_error(
198 return jsonrpc_error(
198 request, retid=request.rpc_id, message='Invalid API KEY')
199 request, retid=request.rpc_id, message='Invalid API KEY')
199
200
200 if not api_user.active:
201 if not api_user.active:
201 return jsonrpc_error(
202 return jsonrpc_error(
202 request, retid=request.rpc_id,
203 request, retid=request.rpc_id,
203 message='Request from this user not allowed')
204 message='Request from this user not allowed')
204
205
205 # check if we are allowed to use this IP
206 # check if we are allowed to use this IP
206 auth_u = AuthUser(
207 auth_u = AuthUser(
207 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 if not auth_u.ip_allowed:
209 if not auth_u.ip_allowed:
209 return jsonrpc_error(
210 return jsonrpc_error(
210 request, retid=request.rpc_id,
211 request, retid=request.rpc_id,
211 message='Request from IP:%s not allowed' % (
212 message='Request from IP:%s not allowed' % (
212 request.rpc_ip_addr,))
213 request.rpc_ip_addr,))
213 else:
214 else:
214 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215
216
216 # register our auth-user
217 # register our auth-user
217 request.rpc_user = auth_u
218 request.rpc_user = auth_u
218 request.environ['rc_auth_user_id'] = auth_u.user_id
219 request.environ['rc_auth_user_id'] = auth_u.user_id
219
220
220 # now check if token is valid for API
221 # now check if token is valid for API
221 auth_token = request.rpc_api_key
222 auth_token = request.rpc_api_key
222 token_match = api_user.authenticate_by_token(
223 token_match = api_user.authenticate_by_token(
223 auth_token, roles=[UserApiKeys.ROLE_API])
224 auth_token, roles=[UserApiKeys.ROLE_API])
224 invalid_token = not token_match
225 invalid_token = not token_match
225
226
226 log.debug('Checking if API KEY is valid with proper role')
227 log.debug('Checking if API KEY is valid with proper role')
227 if invalid_token:
228 if invalid_token:
228 return jsonrpc_error(
229 return jsonrpc_error(
229 request, retid=request.rpc_id,
230 request, retid=request.rpc_id,
230 message='API KEY invalid or, has bad role for an API call')
231 message='API KEY invalid or, has bad role for an API call')
231
232
232 except Exception:
233 except Exception:
233 log.exception('Error on API AUTH')
234 log.exception('Error on API AUTH')
234 return jsonrpc_error(
235 return jsonrpc_error(
235 request, retid=request.rpc_id, message='Invalid API KEY')
236 request, retid=request.rpc_id, message='Invalid API KEY')
236
237
237 method = request.rpc_method
238 method = request.rpc_method
238 func = request.registry.jsonrpc_methods[method]
239 func = request.registry.jsonrpc_methods[method]
239
240
240 # now that we have a method, add request._req_params to
241 # now that we have a method, add request._req_params to
241 # self.kargs and dispatch control to WGIController
242 # self.kargs and dispatch control to WGIController
242 argspec = inspect.getargspec(func)
243 argspec = inspect.getargspec(func)
243 arglist = argspec[0]
244 arglist = argspec[0]
244 defaults = map(type, argspec[3] or [])
245 defaults = map(type, argspec[3] or [])
245 default_empty = types.NotImplementedType
246 default_empty = types.NotImplementedType
246
247
247 # kw arguments required by this method
248 # kw arguments required by this method
248 func_kwargs = dict(itertools.izip_longest(
249 func_kwargs = dict(itertools.izip_longest(
249 reversed(arglist), reversed(defaults), fillvalue=default_empty))
250 reversed(arglist), reversed(defaults), fillvalue=default_empty))
250
251
251 # This attribute will need to be first param of a method that uses
252 # This attribute will need to be first param of a method that uses
252 # api_key, which is translated to instance of user at that name
253 # api_key, which is translated to instance of user at that name
253 user_var = 'apiuser'
254 user_var = 'apiuser'
254 request_var = 'request'
255 request_var = 'request'
255
256
256 for arg in [user_var, request_var]:
257 for arg in [user_var, request_var]:
257 if arg not in arglist:
258 if arg not in arglist:
258 return jsonrpc_error(
259 return jsonrpc_error(
259 request,
260 request,
260 retid=request.rpc_id,
261 retid=request.rpc_id,
261 message='This method [%s] does not support '
262 message='This method [%s] does not support '
262 'required parameter `%s`' % (func.__name__, arg))
263 'required parameter `%s`' % (func.__name__, arg))
263
264
264 # get our arglist and check if we provided them as args
265 # get our arglist and check if we provided them as args
265 for arg, default in func_kwargs.items():
266 for arg, default in func_kwargs.items():
266 if arg in [user_var, request_var]:
267 if arg in [user_var, request_var]:
267 # user_var and request_var are pre-hardcoded parameters and we
268 # user_var and request_var are pre-hardcoded parameters and we
268 # don't need to do any translation
269 # don't need to do any translation
269 continue
270 continue
270
271
271 # skip the required param check if it's default value is
272 # skip the required param check if it's default value is
272 # NotImplementedType (default_empty)
273 # NotImplementedType (default_empty)
273 if default == default_empty and arg not in request.rpc_params:
274 if default == default_empty and arg not in request.rpc_params:
274 return jsonrpc_error(
275 return jsonrpc_error(
275 request,
276 request,
276 retid=request.rpc_id,
277 retid=request.rpc_id,
277 message=('Missing non optional `%s` arg in JSON DATA' % arg)
278 message=('Missing non optional `%s` arg in JSON DATA' % arg)
278 )
279 )
279
280
280 # sanitize extra passed arguments
281 # sanitize extra passed arguments
281 for k in request.rpc_params.keys()[:]:
282 for k in request.rpc_params.keys()[:]:
282 if k not in func_kwargs:
283 if k not in func_kwargs:
283 del request.rpc_params[k]
284 del request.rpc_params[k]
284
285
285 call_params = request.rpc_params
286 call_params = request.rpc_params
286 call_params.update({
287 call_params.update({
287 'request': request,
288 'request': request,
288 'apiuser': auth_u
289 'apiuser': auth_u
289 })
290 })
290
291
291 # register some common functions for usage
292 # register some common functions for usage
292 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
293
294
294 try:
295 try:
295 ret_value = func(**call_params)
296 ret_value = func(**call_params)
296 return jsonrpc_response(request, ret_value)
297 return jsonrpc_response(request, ret_value)
297 except JSONRPCBaseError:
298 except JSONRPCBaseError:
298 raise
299 raise
299 except Exception:
300 except Exception:
300 log.exception('Unhandled exception occurred on api call: %s', func)
301 log.exception('Unhandled exception occurred on api call: %s', func)
301 exc_info = sys.exc_info()
302 exc_info = sys.exc_info()
302 exc_id, exc_type_name = store_exception(
303 exc_id, exc_type_name = store_exception(
303 id(exc_info), exc_info, prefix='rhodecode-api')
304 id(exc_info), exc_info, prefix='rhodecode-api')
304 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
305 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
305 ('RhodeCode-Exception-Type', str(exc_type_name))]
306 ('RhodeCode-Exception-Type', str(exc_type_name))]
306 return jsonrpc_error(
307 return jsonrpc_error(
307 request, retid=request.rpc_id, message='Internal server error',
308 request, retid=request.rpc_id, message='Internal server error',
308 headers=error_headers)
309 headers=error_headers)
309
310
310
311
311 def setup_request(request):
312 def setup_request(request):
312 """
313 """
313 Parse a JSON-RPC request body. It's used inside the predicates method
314 Parse a JSON-RPC request body. It's used inside the predicates method
314 to validate and bootstrap requests for usage in rpc calls.
315 to validate and bootstrap requests for usage in rpc calls.
315
316
316 We need to raise JSONRPCError here if we want to return some errors back to
317 We need to raise JSONRPCError here if we want to return some errors back to
317 user.
318 user.
318 """
319 """
319
320
320 log.debug('Executing setup request: %r', request)
321 log.debug('Executing setup request: %r', request)
321 request.rpc_ip_addr = get_ip_addr(request.environ)
322 request.rpc_ip_addr = get_ip_addr(request.environ)
322 # TODO(marcink): deprecate GET at some point
323 # TODO(marcink): deprecate GET at some point
323 if request.method not in ['POST', 'GET']:
324 if request.method not in ['POST', 'GET']:
324 log.debug('unsupported request method "%s"', request.method)
325 log.debug('unsupported request method "%s"', request.method)
325 raise JSONRPCError(
326 raise JSONRPCError(
326 'unsupported request method "%s". Please use POST' % request.method)
327 'unsupported request method "%s". Please use POST' % request.method)
327
328
328 if 'CONTENT_LENGTH' not in request.environ:
329 if 'CONTENT_LENGTH' not in request.environ:
329 log.debug("No Content-Length")
330 log.debug("No Content-Length")
330 raise JSONRPCError("Empty body, No Content-Length in request")
331 raise JSONRPCError("Empty body, No Content-Length in request")
331
332
332 else:
333 else:
333 length = request.environ['CONTENT_LENGTH']
334 length = request.environ['CONTENT_LENGTH']
334 log.debug('Content-Length: %s', length)
335 log.debug('Content-Length: %s', length)
335
336
336 if length == 0:
337 if length == 0:
337 log.debug("Content-Length is 0")
338 log.debug("Content-Length is 0")
338 raise JSONRPCError("Content-Length is 0")
339 raise JSONRPCError("Content-Length is 0")
339
340
340 raw_body = request.body
341 raw_body = request.body
341 log.debug("Loading JSON body now")
342 log.debug("Loading JSON body now")
342 try:
343 try:
343 json_body = json.loads(raw_body)
344 json_body = json.loads(raw_body)
344 except ValueError as e:
345 except ValueError as e:
345 # catch JSON errors Here
346 # catch JSON errors Here
346 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
347 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
347
348
348 request.rpc_id = json_body.get('id')
349 request.rpc_id = json_body.get('id')
349 request.rpc_method = json_body.get('method')
350 request.rpc_method = json_body.get('method')
350
351
351 # check required base parameters
352 # check required base parameters
352 try:
353 try:
353 api_key = json_body.get('api_key')
354 api_key = json_body.get('api_key')
354 if not api_key:
355 if not api_key:
355 api_key = json_body.get('auth_token')
356 api_key = json_body.get('auth_token')
356
357
357 if not api_key:
358 if not api_key:
358 raise KeyError('api_key or auth_token')
359 raise KeyError('api_key or auth_token')
359
360
360 # TODO(marcink): support passing in token in request header
361 # TODO(marcink): support passing in token in request header
361
362
362 request.rpc_api_key = api_key
363 request.rpc_api_key = api_key
363 request.rpc_id = json_body['id']
364 request.rpc_id = json_body['id']
364 request.rpc_method = json_body['method']
365 request.rpc_method = json_body['method']
365 request.rpc_params = json_body['args'] \
366 request.rpc_params = json_body['args'] \
366 if isinstance(json_body['args'], dict) else {}
367 if isinstance(json_body['args'], dict) else {}
367
368
368 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
369 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
369 except KeyError as e:
370 except KeyError as e:
370 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
371 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
371
372
372 log.debug('setup complete, now handling method:%s rpcid:%s',
373 log.debug('setup complete, now handling method:%s rpcid:%s',
373 request.rpc_method, request.rpc_id, )
374 request.rpc_method, request.rpc_id, )
374
375
375
376
376 class RoutePredicate(object):
377 class RoutePredicate(object):
377 def __init__(self, val, config):
378 def __init__(self, val, config):
378 self.val = val
379 self.val = val
379
380
380 def text(self):
381 def text(self):
381 return 'jsonrpc route = %s' % self.val
382 return 'jsonrpc route = %s' % self.val
382
383
383 phash = text
384 phash = text
384
385
385 def __call__(self, info, request):
386 def __call__(self, info, request):
386 if self.val:
387 if self.val:
387 # potentially setup and bootstrap our call
388 # potentially setup and bootstrap our call
388 setup_request(request)
389 setup_request(request)
389
390
390 # Always return True so that even if it isn't a valid RPC it
391 # Always return True so that even if it isn't a valid RPC it
391 # will fall through to the underlaying handlers like notfound_view
392 # will fall through to the underlaying handlers like notfound_view
392 return True
393 return True
393
394
394
395
395 class NotFoundPredicate(object):
396 class NotFoundPredicate(object):
396 def __init__(self, val, config):
397 def __init__(self, val, config):
397 self.val = val
398 self.val = val
398 self.methods = config.registry.jsonrpc_methods
399 self.methods = config.registry.jsonrpc_methods
399
400
400 def text(self):
401 def text(self):
401 return 'jsonrpc method not found = {}.'.format(self.val)
402 return 'jsonrpc method not found = {}.'.format(self.val)
402
403
403 phash = text
404 phash = text
404
405
405 def __call__(self, info, request):
406 def __call__(self, info, request):
406 return hasattr(request, 'rpc_method')
407 return hasattr(request, 'rpc_method')
407
408
408
409
409 class MethodPredicate(object):
410 class MethodPredicate(object):
410 def __init__(self, val, config):
411 def __init__(self, val, config):
411 self.method = val
412 self.method = val
412
413
413 def text(self):
414 def text(self):
414 return 'jsonrpc method = %s' % self.method
415 return 'jsonrpc method = %s' % self.method
415
416
416 phash = text
417 phash = text
417
418
418 def __call__(self, context, request):
419 def __call__(self, context, request):
419 # we need to explicitly return False here, so pyramid doesn't try to
420 # we need to explicitly return False here, so pyramid doesn't try to
420 # execute our view directly. We need our main handler to execute things
421 # execute our view directly. We need our main handler to execute things
421 return getattr(request, 'rpc_method') == self.method
422 return getattr(request, 'rpc_method') == self.method
422
423
423
424
424 def add_jsonrpc_method(config, view, **kwargs):
425 def add_jsonrpc_method(config, view, **kwargs):
425 # pop the method name
426 # pop the method name
426 method = kwargs.pop('method', None)
427 method = kwargs.pop('method', None)
427
428
428 if method is None:
429 if method is None:
429 raise ConfigurationError(
430 raise ConfigurationError(
430 'Cannot register a JSON-RPC method without specifying the "method"')
431 'Cannot register a JSON-RPC method without specifying the "method"')
431
432
432 # we define custom predicate, to enable to detect conflicting methods,
433 # we define custom predicate, to enable to detect conflicting methods,
433 # those predicates are kind of "translation" from the decorator variables
434 # those predicates are kind of "translation" from the decorator variables
434 # to internal predicates names
435 # to internal predicates names
435
436
436 kwargs['jsonrpc_method'] = method
437 kwargs['jsonrpc_method'] = method
437
438
438 # register our view into global view store for validation
439 # register our view into global view store for validation
439 config.registry.jsonrpc_methods[method] = view
440 config.registry.jsonrpc_methods[method] = view
440
441
441 # we're using our main request_view handler, here, so each method
442 # we're using our main request_view handler, here, so each method
442 # has a unified handler for itself
443 # has a unified handler for itself
443 config.add_view(request_view, route_name='apiv2', **kwargs)
444 config.add_view(request_view, route_name='apiv2', **kwargs)
444
445
445
446
446 class jsonrpc_method(object):
447 class jsonrpc_method(object):
447 """
448 """
448 decorator that works similar to @add_view_config decorator,
449 decorator that works similar to @add_view_config decorator,
449 but tailored for our JSON RPC
450 but tailored for our JSON RPC
450 """
451 """
451
452
452 venusian = venusian # for testing injection
453 venusian = venusian # for testing injection
453
454
454 def __init__(self, method=None, **kwargs):
455 def __init__(self, method=None, **kwargs):
455 self.method = method
456 self.method = method
456 self.kwargs = kwargs
457 self.kwargs = kwargs
457
458
458 def __call__(self, wrapped):
459 def __call__(self, wrapped):
459 kwargs = self.kwargs.copy()
460 kwargs = self.kwargs.copy()
460 kwargs['method'] = self.method or wrapped.__name__
461 kwargs['method'] = self.method or wrapped.__name__
461 depth = kwargs.pop('_depth', 0)
462 depth = kwargs.pop('_depth', 0)
462
463
463 def callback(context, name, ob):
464 def callback(context, name, ob):
464 config = context.config.with_package(info.module)
465 config = context.config.with_package(info.module)
465 config.add_jsonrpc_method(view=ob, **kwargs)
466 config.add_jsonrpc_method(view=ob, **kwargs)
466
467
467 info = venusian.attach(wrapped, callback, category='pyramid',
468 info = venusian.attach(wrapped, callback, category='pyramid',
468 depth=depth + 1)
469 depth=depth + 1)
469 if info.scope == 'class':
470 if info.scope == 'class':
470 # ensure that attr is set if decorating a class method
471 # ensure that attr is set if decorating a class method
471 kwargs.setdefault('attr', wrapped.__name__)
472 kwargs.setdefault('attr', wrapped.__name__)
472
473
473 kwargs['_info'] = info.codeinfo # fbo action_method
474 kwargs['_info'] = info.codeinfo # fbo action_method
474 return wrapped
475 return wrapped
475
476
476
477
477 class jsonrpc_deprecated_method(object):
478 class jsonrpc_deprecated_method(object):
478 """
479 """
479 Marks method as deprecated, adds log.warning, and inject special key to
480 Marks method as deprecated, adds log.warning, and inject special key to
480 the request variable to mark method as deprecated.
481 the request variable to mark method as deprecated.
481 Also injects special docstring that extract_docs will catch to mark
482 Also injects special docstring that extract_docs will catch to mark
482 method as deprecated.
483 method as deprecated.
483
484
484 :param use_method: specify which method should be used instead of
485 :param use_method: specify which method should be used instead of
485 the decorated one
486 the decorated one
486
487
487 Use like::
488 Use like::
488
489
489 @jsonrpc_method()
490 @jsonrpc_method()
490 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
491 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
491 def old_func(request, apiuser, arg1, arg2):
492 def old_func(request, apiuser, arg1, arg2):
492 ...
493 ...
493 """
494 """
494
495
495 def __init__(self, use_method, deprecated_at_version):
496 def __init__(self, use_method, deprecated_at_version):
496 self.use_method = use_method
497 self.use_method = use_method
497 self.deprecated_at_version = deprecated_at_version
498 self.deprecated_at_version = deprecated_at_version
498 self.deprecated_msg = ''
499 self.deprecated_msg = ''
499
500
500 def __call__(self, func):
501 def __call__(self, func):
501 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
502 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
502 method=self.use_method)
503 method=self.use_method)
503
504
504 docstring = """\n
505 docstring = """\n
505 .. deprecated:: {version}
506 .. deprecated:: {version}
506
507
507 {deprecation_message}
508 {deprecation_message}
508
509
509 {original_docstring}
510 {original_docstring}
510 """
511 """
511 func.__doc__ = docstring.format(
512 func.__doc__ = docstring.format(
512 version=self.deprecated_at_version,
513 version=self.deprecated_at_version,
513 deprecation_message=self.deprecated_msg,
514 deprecation_message=self.deprecated_msg,
514 original_docstring=func.__doc__)
515 original_docstring=func.__doc__)
515 return decorator.decorator(self.__wrapper, func)
516 return decorator.decorator(self.__wrapper, func)
516
517
517 def __wrapper(self, func, *fargs, **fkwargs):
518 def __wrapper(self, func, *fargs, **fkwargs):
518 log.warning('DEPRECATED API CALL on function %s, please '
519 log.warning('DEPRECATED API CALL on function %s, please '
519 'use `%s` instead', func, self.use_method)
520 'use `%s` instead', func, self.use_method)
520 # alter function docstring to mark as deprecated, this is picked up
521 # alter function docstring to mark as deprecated, this is picked up
521 # via fabric file that generates API DOC.
522 # via fabric file that generates API DOC.
522 result = func(*fargs, **fkwargs)
523 result = func(*fargs, **fkwargs)
523
524
524 request = fargs[0]
525 request = fargs[0]
525 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
526 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
526 return result
527 return result
527
528
528
529
529 def includeme(config):
530 def includeme(config):
530 plugin_module = 'rhodecode.api'
531 plugin_module = 'rhodecode.api'
531 plugin_settings = get_plugin_settings(
532 plugin_settings = get_plugin_settings(
532 plugin_module, config.registry.settings)
533 plugin_module, config.registry.settings)
533
534
534 if not hasattr(config.registry, 'jsonrpc_methods'):
535 if not hasattr(config.registry, 'jsonrpc_methods'):
535 config.registry.jsonrpc_methods = OrderedDict()
536 config.registry.jsonrpc_methods = OrderedDict()
536
537
537 # match filter by given method only
538 # match filter by given method only
538 config.add_view_predicate('jsonrpc_method', MethodPredicate)
539 config.add_view_predicate('jsonrpc_method', MethodPredicate)
539 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
540 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
540
541
541 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
542 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
542 serializer=json.dumps, indent=4))
543 serializer=json.dumps, indent=4))
543 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
544 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
544
545
545 config.add_route_predicate(
546 config.add_route_predicate(
546 'jsonrpc_call', RoutePredicate)
547 'jsonrpc_call', RoutePredicate)
547
548
548 config.add_route(
549 config.add_route(
549 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
550 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
550
551
551 config.scan(plugin_module, ignore='rhodecode.api.tests')
552 config.scan(plugin_module, ignore='rhodecode.api.tests')
552 # register some exception handling view
553 # register some exception handling view
553 config.add_view(exception_view, context=JSONRPCBaseError)
554 config.add_view(exception_view, context=JSONRPCBaseError)
554 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
555 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,60 +1,61 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
25
25
26
26
27 @pytest.mark.usefixtures("testuser_api", "app")
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestGetMethod(object):
28 class TestGetMethod(object):
29 def test_get_methods_no_matches(self):
29 def test_get_methods_no_matches(self):
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
31 response = api_call(self.app, params)
31 response = api_call(self.app, params)
32
32
33 expected = []
33 expected = []
34 assert_ok(id_, expected, given=response.body)
34 assert_ok(id_, expected, given=response.body)
35
35
36 def test_get_methods(self):
36 def test_get_methods(self):
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 response = api_call(self.app, params)
38 response = api_call(self.app, params)
39
39
40 expected = ['changeset_comment', 'comment_pull_request',
40 expected = ['changeset_comment', 'comment_pull_request',
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
42 assert_ok(id_, expected, given=response.body)
42 assert_ok(id_, expected, given=response.body)
43
43
44 def test_get_methods_on_single_match(self):
44 def test_get_methods_on_single_match(self):
45 id_, params = build_data(self.apikey, 'get_method',
45 id_, params = build_data(self.apikey, 'get_method',
46 pattern='*comment_commit*')
46 pattern='*comment_commit*')
47 response = api_call(self.app, params)
47 response = api_call(self.app, params)
48
48
49 expected = ['comment_commit',
49 expected = ['comment_commit',
50 {'apiuser': '<RequiredType>',
50 {'apiuser': '<RequiredType>',
51 'comment_type': "<Optional:u'note'>",
51 'comment_type': "<Optional:u'note'>",
52 'commit_id': '<RequiredType>',
52 'commit_id': '<RequiredType>',
53 'extra_recipients': '<Optional:[]>',
53 'extra_recipients': '<Optional:[]>',
54 'message': '<RequiredType>',
54 'message': '<RequiredType>',
55 'repoid': '<RequiredType>',
55 'repoid': '<RequiredType>',
56 'request': '<RequiredType>',
56 'request': '<RequiredType>',
57 'resolves_comment_id': '<Optional:None>',
57 'resolves_comment_id': '<Optional:None>',
58 'status': '<Optional:None>',
58 'status': '<Optional:None>',
59 'userid': '<Optional:<OptionalAttr:apiuser>>'}]
59 'userid': '<Optional:<OptionalAttr:apiuser>>',
60 'send_email': '<Optional:True>'}]
60 assert_ok(id_, expected, given=response.body)
61 assert_ok(id_, expected, given=response.body)
@@ -1,90 +1,90 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.repo import RepoModel
24 from rhodecode.model.repo import RepoModel
25 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_error, assert_ok, crash)
26 build_data, api_call, assert_error, assert_ok, crash)
27
27
28
28
29 @pytest.mark.usefixtures("testuser_api", "app")
29 @pytest.mark.usefixtures("testuser_api", "app")
30 class TestGrantUserGroupPermission(object):
30 class TestGrantUserGroupPermission(object):
31 @pytest.mark.parametrize("name, perm", [
31 @pytest.mark.parametrize("name, perm", [
32 ('none', 'repository.none'),
32 ('none', 'repository.none'),
33 ('read', 'repository.read'),
33 ('read', 'repository.read'),
34 ('write', 'repository.write'),
34 ('write', 'repository.write'),
35 ('admin', 'repository.admin')
35 ('admin', 'repository.admin')
36 ])
36 ])
37 def test_api_grant_user_group_permission(
37 def test_api_grant_user_group_permission(
38 self, name, perm, backend, user_util):
38 self, name, perm, backend, user_util):
39 user_group = user_util.create_user_group()
39 user_group = user_util.create_user_group()
40 id_, params = build_data(
40 id_, params = build_data(
41 self.apikey,
41 self.apikey,
42 'grant_user_group_permission',
42 'grant_user_group_permission',
43 repoid=backend.repo_name,
43 repoid=backend.repo_name,
44 usergroupid=user_group.users_group_name,
44 usergroupid=user_group.users_group_name,
45 perm=perm)
45 perm=perm)
46 response = api_call(self.app, params)
46 response = api_call(self.app, params)
47
47
48 ret = {
48 ret = {
49 'msg': 'Granted perm: `%s` for user group: `%s` in repo: `%s`' % (
49 'msg': 'Granted perm: `%s` for user group: `%s` in repo: `%s`' % (
50 perm, user_group.users_group_name, backend.repo_name
50 perm, user_group.users_group_name, backend.repo_name
51 ),
51 ),
52 'success': True
52 'success': True
53 }
53 }
54 expected = ret
54 expected = ret
55 assert_ok(id_, expected, given=response.body)
55 assert_ok(id_, expected, given=response.body)
56
56
57 def test_api_grant_user_group_permission_wrong_permission(
57 def test_api_grant_user_group_permission_wrong_permission(
58 self, backend, user_util):
58 self, backend, user_util):
59 perm = 'haha.no.permission'
59 perm = 'haha.no.permission'
60 user_group = user_util.create_user_group()
60 user_group = user_util.create_user_group()
61 id_, params = build_data(
61 id_, params = build_data(
62 self.apikey,
62 self.apikey,
63 'grant_user_group_permission',
63 'grant_user_group_permission',
64 repoid=backend.repo_name,
64 repoid=backend.repo_name,
65 usergroupid=user_group.users_group_name,
65 usergroupid=user_group.users_group_name,
66 perm=perm)
66 perm=perm)
67 response = api_call(self.app, params)
67 response = api_call(self.app, params)
68
68
69 expected = 'permission `%s` does not exist' % (perm,)
69 expected = 'permission `%s` does not exist.' % (perm,)
70 assert_error(id_, expected, given=response.body)
70 assert_error(id_, expected, given=response.body)
71
71
72 @mock.patch.object(RepoModel, 'grant_user_group_permission', crash)
72 @mock.patch.object(RepoModel, 'grant_user_group_permission', crash)
73 def test_api_grant_user_group_permission_exception_when_adding(
73 def test_api_grant_user_group_permission_exception_when_adding(
74 self, backend, user_util):
74 self, backend, user_util):
75 perm = 'repository.read'
75 perm = 'repository.read'
76 user_group = user_util.create_user_group()
76 user_group = user_util.create_user_group()
77 id_, params = build_data(
77 id_, params = build_data(
78 self.apikey,
78 self.apikey,
79 'grant_user_group_permission',
79 'grant_user_group_permission',
80 repoid=backend.repo_name,
80 repoid=backend.repo_name,
81 usergroupid=user_group.users_group_name,
81 usergroupid=user_group.users_group_name,
82 perm=perm)
82 perm=perm)
83 response = api_call(self.app, params)
83 response = api_call(self.app, params)
84
84
85 expected = (
85 expected = (
86 'failed to edit permission for user group: `%s` in repo: `%s`' % (
86 'failed to edit permission for user group: `%s` in repo: `%s`' % (
87 user_group.users_group_name, backend.repo_name
87 user_group.users_group_name, backend.repo_name
88 )
88 )
89 )
89 )
90 assert_error(id_, expected, given=response.body)
90 assert_error(id_, expected, given=response.body)
@@ -1,174 +1,173 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.user import UserModel
24 from rhodecode.model.user import UserModel
25 from rhodecode.model.repo_group import RepoGroupModel
25 from rhodecode.model.repo_group import RepoGroupModel
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok, crash)
27 build_data, api_call, assert_error, assert_ok, crash)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestGrantUserGroupPermissionFromRepoGroup(object):
31 class TestGrantUserGroupPermissionFromRepoGroup(object):
32 @pytest.mark.parametrize("name, perm, apply_to_children", [
32 @pytest.mark.parametrize("name, perm, apply_to_children", [
33 ('none', 'group.none', 'none'),
33 ('none', 'group.none', 'none'),
34 ('read', 'group.read', 'none'),
34 ('read', 'group.read', 'none'),
35 ('write', 'group.write', 'none'),
35 ('write', 'group.write', 'none'),
36 ('admin', 'group.admin', 'none'),
36 ('admin', 'group.admin', 'none'),
37
37
38 ('none', 'group.none', 'all'),
38 ('none', 'group.none', 'all'),
39 ('read', 'group.read', 'all'),
39 ('read', 'group.read', 'all'),
40 ('write', 'group.write', 'all'),
40 ('write', 'group.write', 'all'),
41 ('admin', 'group.admin', 'all'),
41 ('admin', 'group.admin', 'all'),
42
42
43 ('none', 'group.none', 'repos'),
43 ('none', 'group.none', 'repos'),
44 ('read', 'group.read', 'repos'),
44 ('read', 'group.read', 'repos'),
45 ('write', 'group.write', 'repos'),
45 ('write', 'group.write', 'repos'),
46 ('admin', 'group.admin', 'repos'),
46 ('admin', 'group.admin', 'repos'),
47
47
48 ('none', 'group.none', 'groups'),
48 ('none', 'group.none', 'groups'),
49 ('read', 'group.read', 'groups'),
49 ('read', 'group.read', 'groups'),
50 ('write', 'group.write', 'groups'),
50 ('write', 'group.write', 'groups'),
51 ('admin', 'group.admin', 'groups'),
51 ('admin', 'group.admin', 'groups'),
52 ])
52 ])
53 def test_api_grant_user_group_permission_to_repo_group(
53 def test_api_grant_user_group_permission_to_repo_group(
54 self, name, perm, apply_to_children, user_util):
54 self, name, perm, apply_to_children, user_util):
55 user_group = user_util.create_user_group()
55 user_group = user_util.create_user_group()
56 repo_group = user_util.create_repo_group()
56 repo_group = user_util.create_repo_group()
57 user_util.create_repo(parent=repo_group)
57 user_util.create_repo(parent=repo_group)
58
58
59 id_, params = build_data(
59 id_, params = build_data(
60 self.apikey,
60 self.apikey,
61 'grant_user_group_permission_to_repo_group',
61 'grant_user_group_permission_to_repo_group',
62 repogroupid=repo_group.name,
62 repogroupid=repo_group.name,
63 usergroupid=user_group.users_group_name,
63 usergroupid=user_group.users_group_name,
64 perm=perm,
64 perm=perm,
65 apply_to_children=apply_to_children,)
65 apply_to_children=apply_to_children,)
66 response = api_call(self.app, params)
66 response = api_call(self.app, params)
67
67
68 ret = {
68 ret = {
69 'msg': (
69 'msg': (
70 'Granted perm: `%s` (recursive:%s) for user group: `%s`'
70 'Granted perm: `%s` (recursive:%s) for user group: `%s`'
71 ' in repo group: `%s`' % (
71 ' in repo group: `%s`' % (
72 perm, apply_to_children, user_group.users_group_name,
72 perm, apply_to_children, user_group.users_group_name,
73 repo_group.name
73 repo_group.name
74 )
74 )
75 ),
75 ),
76 'success': True
76 'success': True
77 }
77 }
78 expected = ret
78 expected = ret
79 try:
79 try:
80 assert_ok(id_, expected, given=response.body)
80 assert_ok(id_, expected, given=response.body)
81 finally:
81 finally:
82 RepoGroupModel().revoke_user_group_permission(
82 RepoGroupModel().revoke_user_group_permission(
83 repo_group.group_id, user_group.users_group_id)
83 repo_group.group_id, user_group.users_group_id)
84
84
85 @pytest.mark.parametrize(
85 @pytest.mark.parametrize(
86 "name, perm, apply_to_children, grant_admin, access_ok", [
86 "name, perm, apply_to_children, grant_admin, access_ok", [
87 ('none_fails', 'group.none', 'none', False, False),
87 ('none_fails', 'group.none', 'none', False, False),
88 ('read_fails', 'group.read', 'none', False, False),
88 ('read_fails', 'group.read', 'none', False, False),
89 ('write_fails', 'group.write', 'none', False, False),
89 ('write_fails', 'group.write', 'none', False, False),
90 ('admin_fails', 'group.admin', 'none', False, False),
90 ('admin_fails', 'group.admin', 'none', False, False),
91
91
92 # with granted perms
92 # with granted perms
93 ('none_ok', 'group.none', 'none', True, True),
93 ('none_ok', 'group.none', 'none', True, True),
94 ('read_ok', 'group.read', 'none', True, True),
94 ('read_ok', 'group.read', 'none', True, True),
95 ('write_ok', 'group.write', 'none', True, True),
95 ('write_ok', 'group.write', 'none', True, True),
96 ('admin_ok', 'group.admin', 'none', True, True),
96 ('admin_ok', 'group.admin', 'none', True, True),
97 ]
97 ]
98 )
98 )
99 def test_api_grant_user_group_permission_to_repo_group_by_regular_user(
99 def test_api_grant_user_group_permission_to_repo_group_by_regular_user(
100 self, name, perm, apply_to_children, grant_admin, access_ok,
100 self, name, perm, apply_to_children, grant_admin, access_ok,
101 user_util):
101 user_util):
102 user = UserModel().get_by_username(self.TEST_USER_LOGIN)
102 user = UserModel().get_by_username(self.TEST_USER_LOGIN)
103 user_group = user_util.create_user_group()
103 user_group = user_util.create_user_group()
104 repo_group = user_util.create_repo_group()
104 repo_group = user_util.create_repo_group()
105 if grant_admin:
105 if grant_admin:
106 user_util.grant_user_permission_to_repo_group(
106 user_util.grant_user_permission_to_repo_group(
107 repo_group, user, 'group.admin')
107 repo_group, user, 'group.admin')
108
108
109 id_, params = build_data(
109 id_, params = build_data(
110 self.apikey_regular,
110 self.apikey_regular,
111 'grant_user_group_permission_to_repo_group',
111 'grant_user_group_permission_to_repo_group',
112 repogroupid=repo_group.name,
112 repogroupid=repo_group.name,
113 usergroupid=user_group.users_group_name,
113 usergroupid=user_group.users_group_name,
114 perm=perm,
114 perm=perm,
115 apply_to_children=apply_to_children,)
115 apply_to_children=apply_to_children,)
116 response = api_call(self.app, params)
116 response = api_call(self.app, params)
117 if access_ok:
117 if access_ok:
118 ret = {
118 ret = {
119 'msg': (
119 'msg': (
120 'Granted perm: `%s` (recursive:%s) for user group: `%s`'
120 'Granted perm: `%s` (recursive:%s) for user group: `%s`'
121 ' in repo group: `%s`' % (
121 ' in repo group: `%s`' % (
122 perm, apply_to_children, user_group.users_group_name,
122 perm, apply_to_children, user_group.users_group_name,
123 repo_group.name
123 repo_group.name
124 )
124 )
125 ),
125 ),
126 'success': True
126 'success': True
127 }
127 }
128 expected = ret
128 expected = ret
129 try:
129 try:
130 assert_ok(id_, expected, given=response.body)
130 assert_ok(id_, expected, given=response.body)
131 finally:
131 finally:
132 RepoGroupModel().revoke_user_group_permission(
132 RepoGroupModel().revoke_user_group_permission(
133 repo_group.group_id, user_group.users_group_id)
133 repo_group.group_id, user_group.users_group_id)
134 else:
134 else:
135 expected = 'repository group `%s` does not exist' % (
135 expected = 'repository group `%s` does not exist' % (repo_group.name,)
136 repo_group.name,)
137 assert_error(id_, expected, given=response.body)
136 assert_error(id_, expected, given=response.body)
138
137
139 def test_api_grant_user_group_permission_to_repo_group_wrong_permission(
138 def test_api_grant_user_group_permission_to_repo_group_wrong_permission(
140 self, user_util):
139 self, user_util):
141 user_group = user_util.create_user_group()
140 user_group = user_util.create_user_group()
142 repo_group = user_util.create_repo_group()
141 repo_group = user_util.create_repo_group()
143 perm = 'haha.no.permission'
142 perm = 'haha.no.permission'
144 id_, params = build_data(
143 id_, params = build_data(
145 self.apikey,
144 self.apikey,
146 'grant_user_group_permission_to_repo_group',
145 'grant_user_group_permission_to_repo_group',
147 repogroupid=repo_group.name,
146 repogroupid=repo_group.name,
148 usergroupid=user_group.users_group_name,
147 usergroupid=user_group.users_group_name,
149 perm=perm)
148 perm=perm)
150 response = api_call(self.app, params)
149 response = api_call(self.app, params)
151
150
152 expected = 'permission `%s` does not exist' % (perm,)
151 expected = 'permission `%s` does not exist. Permission should start with prefix: `group.`' % (perm,)
153 assert_error(id_, expected, given=response.body)
152 assert_error(id_, expected, given=response.body)
154
153
155 @mock.patch.object(RepoGroupModel, 'grant_user_group_permission', crash)
154 @mock.patch.object(RepoGroupModel, 'grant_user_group_permission', crash)
156 def test_api_grant_user_group_permission_exception_when_adding_2(
155 def test_api_grant_user_group_permission_exception_when_adding_2(
157 self, user_util):
156 self, user_util):
158 user_group = user_util.create_user_group()
157 user_group = user_util.create_user_group()
159 repo_group = user_util.create_repo_group()
158 repo_group = user_util.create_repo_group()
160 perm = 'group.read'
159 perm = 'group.read'
161 id_, params = build_data(
160 id_, params = build_data(
162 self.apikey,
161 self.apikey,
163 'grant_user_group_permission_to_repo_group',
162 'grant_user_group_permission_to_repo_group',
164 repogroupid=repo_group.name,
163 repogroupid=repo_group.name,
165 usergroupid=user_group.users_group_name,
164 usergroupid=user_group.users_group_name,
166 perm=perm)
165 perm=perm)
167 response = api_call(self.app, params)
166 response = api_call(self.app, params)
168
167
169 expected = (
168 expected = (
170 'failed to edit permission for user group: `%s`'
169 'failed to edit permission for user group: `%s`'
171 ' in repo group: `%s`' % (
170 ' in repo group: `%s`' % (
172 user_group.users_group_name, repo_group.name)
171 user_group.users_group_name, repo_group.name)
173 )
172 )
174 assert_error(id_, expected, given=response.body)
173 assert_error(id_, expected, given=response.body)
@@ -1,87 +1,87 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.repo import RepoModel
24 from rhodecode.model.repo import RepoModel
25 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_error, assert_ok, crash)
26 build_data, api_call, assert_error, assert_ok, crash)
27
27
28
28
29 @pytest.mark.usefixtures("testuser_api", "app")
29 @pytest.mark.usefixtures("testuser_api", "app")
30 class TestGrantUserPermission(object):
30 class TestGrantUserPermission(object):
31 @pytest.mark.parametrize("name, perm", [
31 @pytest.mark.parametrize("name, perm", [
32 ('none', 'repository.none'),
32 ('none', 'repository.none'),
33 ('read', 'repository.read'),
33 ('read', 'repository.read'),
34 ('write', 'repository.write'),
34 ('write', 'repository.write'),
35 ('admin', 'repository.admin')
35 ('admin', 'repository.admin')
36 ])
36 ])
37 def test_api_grant_user_permission(self, name, perm, backend, user_util):
37 def test_api_grant_user_permission(self, name, perm, backend, user_util):
38 user = user_util.create_user()
38 user = user_util.create_user()
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey,
40 self.apikey,
41 'grant_user_permission',
41 'grant_user_permission',
42 repoid=backend.repo_name,
42 repoid=backend.repo_name,
43 userid=user.username,
43 userid=user.username,
44 perm=perm)
44 perm=perm)
45 response = api_call(self.app, params)
45 response = api_call(self.app, params)
46
46
47 ret = {
47 ret = {
48 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
48 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
49 perm, user.username, backend.repo_name
49 perm, user.username, backend.repo_name
50 ),
50 ),
51 'success': True
51 'success': True
52 }
52 }
53 expected = ret
53 expected = ret
54 assert_ok(id_, expected, given=response.body)
54 assert_ok(id_, expected, given=response.body)
55
55
56 def test_api_grant_user_permission_wrong_permission(
56 def test_api_grant_user_permission_wrong_permission(
57 self, backend, user_util):
57 self, backend, user_util):
58 user = user_util.create_user()
58 user = user_util.create_user()
59 perm = 'haha.no.permission'
59 perm = 'haha.no.permission'
60 id_, params = build_data(
60 id_, params = build_data(
61 self.apikey,
61 self.apikey,
62 'grant_user_permission',
62 'grant_user_permission',
63 repoid=backend.repo_name,
63 repoid=backend.repo_name,
64 userid=user.username,
64 userid=user.username,
65 perm=perm)
65 perm=perm)
66 response = api_call(self.app, params)
66 response = api_call(self.app, params)
67
67
68 expected = 'permission `%s` does not exist' % (perm,)
68 expected = 'permission `%s` does not exist.' % (perm,)
69 assert_error(id_, expected, given=response.body)
69 assert_error(id_, expected, given=response.body)
70
70
71 @mock.patch.object(RepoModel, 'grant_user_permission', crash)
71 @mock.patch.object(RepoModel, 'grant_user_permission', crash)
72 def test_api_grant_user_permission_exception_when_adding(
72 def test_api_grant_user_permission_exception_when_adding(
73 self, backend, user_util):
73 self, backend, user_util):
74 user = user_util.create_user()
74 user = user_util.create_user()
75 perm = 'repository.read'
75 perm = 'repository.read'
76 id_, params = build_data(
76 id_, params = build_data(
77 self.apikey,
77 self.apikey,
78 'grant_user_permission',
78 'grant_user_permission',
79 repoid=backend.repo_name,
79 repoid=backend.repo_name,
80 userid=user.username,
80 userid=user.username,
81 perm=perm)
81 perm=perm)
82 response = api_call(self.app, params)
82 response = api_call(self.app, params)
83
83
84 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
84 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
85 user.username, backend.repo_name
85 user.username, backend.repo_name
86 )
86 )
87 assert_error(id_, expected, given=response.body)
87 assert_error(id_, expected, given=response.body)
@@ -1,157 +1,157 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.user import UserModel
24 from rhodecode.model.user import UserModel
25 from rhodecode.model.repo_group import RepoGroupModel
25 from rhodecode.model.repo_group import RepoGroupModel
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok, crash)
27 build_data, api_call, assert_error, assert_ok, crash)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestGrantUserPermissionFromRepoGroup(object):
31 class TestGrantUserPermissionFromRepoGroup(object):
32 @pytest.mark.parametrize("name, perm, apply_to_children", [
32 @pytest.mark.parametrize("name, perm, apply_to_children", [
33 ('none', 'group.none', 'none'),
33 ('none', 'group.none', 'none'),
34 ('read', 'group.read', 'none'),
34 ('read', 'group.read', 'none'),
35 ('write', 'group.write', 'none'),
35 ('write', 'group.write', 'none'),
36 ('admin', 'group.admin', 'none'),
36 ('admin', 'group.admin', 'none'),
37
37
38 ('none', 'group.none', 'all'),
38 ('none', 'group.none', 'all'),
39 ('read', 'group.read', 'all'),
39 ('read', 'group.read', 'all'),
40 ('write', 'group.write', 'all'),
40 ('write', 'group.write', 'all'),
41 ('admin', 'group.admin', 'all'),
41 ('admin', 'group.admin', 'all'),
42
42
43 ('none', 'group.none', 'repos'),
43 ('none', 'group.none', 'repos'),
44 ('read', 'group.read', 'repos'),
44 ('read', 'group.read', 'repos'),
45 ('write', 'group.write', 'repos'),
45 ('write', 'group.write', 'repos'),
46 ('admin', 'group.admin', 'repos'),
46 ('admin', 'group.admin', 'repos'),
47
47
48 ('none', 'group.none', 'groups'),
48 ('none', 'group.none', 'groups'),
49 ('read', 'group.read', 'groups'),
49 ('read', 'group.read', 'groups'),
50 ('write', 'group.write', 'groups'),
50 ('write', 'group.write', 'groups'),
51 ('admin', 'group.admin', 'groups'),
51 ('admin', 'group.admin', 'groups'),
52 ])
52 ])
53 def test_api_grant_user_permission_to_repo_group(
53 def test_api_grant_user_permission_to_repo_group(
54 self, name, perm, apply_to_children, user_util):
54 self, name, perm, apply_to_children, user_util):
55 user = user_util.create_user()
55 user = user_util.create_user()
56 repo_group = user_util.create_repo_group()
56 repo_group = user_util.create_repo_group()
57 id_, params = build_data(
57 id_, params = build_data(
58 self.apikey, 'grant_user_permission_to_repo_group',
58 self.apikey, 'grant_user_permission_to_repo_group',
59 repogroupid=repo_group.name, userid=user.username,
59 repogroupid=repo_group.name, userid=user.username,
60 perm=perm, apply_to_children=apply_to_children)
60 perm=perm, apply_to_children=apply_to_children)
61 response = api_call(self.app, params)
61 response = api_call(self.app, params)
62
62
63 ret = {
63 ret = {
64 'msg': (
64 'msg': (
65 'Granted perm: `%s` (recursive:%s) for user: `%s`'
65 'Granted perm: `%s` (recursive:%s) for user: `%s`'
66 ' in repo group: `%s`' % (
66 ' in repo group: `%s`' % (
67 perm, apply_to_children, user.username, repo_group.name
67 perm, apply_to_children, user.username, repo_group.name
68 )
68 )
69 ),
69 ),
70 'success': True
70 'success': True
71 }
71 }
72 expected = ret
72 expected = ret
73 assert_ok(id_, expected, given=response.body)
73 assert_ok(id_, expected, given=response.body)
74
74
75 @pytest.mark.parametrize(
75 @pytest.mark.parametrize(
76 "name, perm, apply_to_children, grant_admin, access_ok", [
76 "name, perm, apply_to_children, grant_admin, access_ok", [
77 ('none_fails', 'group.none', 'none', False, False),
77 ('none_fails', 'group.none', 'none', False, False),
78 ('read_fails', 'group.read', 'none', False, False),
78 ('read_fails', 'group.read', 'none', False, False),
79 ('write_fails', 'group.write', 'none', False, False),
79 ('write_fails', 'group.write', 'none', False, False),
80 ('admin_fails', 'group.admin', 'none', False, False),
80 ('admin_fails', 'group.admin', 'none', False, False),
81
81
82 # with granted perms
82 # with granted perms
83 ('none_ok', 'group.none', 'none', True, True),
83 ('none_ok', 'group.none', 'none', True, True),
84 ('read_ok', 'group.read', 'none', True, True),
84 ('read_ok', 'group.read', 'none', True, True),
85 ('write_ok', 'group.write', 'none', True, True),
85 ('write_ok', 'group.write', 'none', True, True),
86 ('admin_ok', 'group.admin', 'none', True, True),
86 ('admin_ok', 'group.admin', 'none', True, True),
87 ]
87 ]
88 )
88 )
89 def test_api_grant_user_permission_to_repo_group_by_regular_user(
89 def test_api_grant_user_permission_to_repo_group_by_regular_user(
90 self, name, perm, apply_to_children, grant_admin, access_ok,
90 self, name, perm, apply_to_children, grant_admin, access_ok,
91 user_util):
91 user_util):
92 user = user_util.create_user()
92 user = user_util.create_user()
93 repo_group = user_util.create_repo_group()
93 repo_group = user_util.create_repo_group()
94
94
95 if grant_admin:
95 if grant_admin:
96 test_user = UserModel().get_by_username(self.TEST_USER_LOGIN)
96 test_user = UserModel().get_by_username(self.TEST_USER_LOGIN)
97 user_util.grant_user_permission_to_repo_group(
97 user_util.grant_user_permission_to_repo_group(
98 repo_group, test_user, 'group.admin')
98 repo_group, test_user, 'group.admin')
99
99
100 id_, params = build_data(
100 id_, params = build_data(
101 self.apikey_regular, 'grant_user_permission_to_repo_group',
101 self.apikey_regular, 'grant_user_permission_to_repo_group',
102 repogroupid=repo_group.name, userid=user.username,
102 repogroupid=repo_group.name, userid=user.username,
103 perm=perm, apply_to_children=apply_to_children)
103 perm=perm, apply_to_children=apply_to_children)
104 response = api_call(self.app, params)
104 response = api_call(self.app, params)
105 if access_ok:
105 if access_ok:
106 ret = {
106 ret = {
107 'msg': (
107 'msg': (
108 'Granted perm: `%s` (recursive:%s) for user: `%s`'
108 'Granted perm: `%s` (recursive:%s) for user: `%s`'
109 ' in repo group: `%s`' % (
109 ' in repo group: `%s`' % (
110 perm, apply_to_children, user.username, repo_group.name
110 perm, apply_to_children, user.username, repo_group.name
111 )
111 )
112 ),
112 ),
113 'success': True
113 'success': True
114 }
114 }
115 expected = ret
115 expected = ret
116 assert_ok(id_, expected, given=response.body)
116 assert_ok(id_, expected, given=response.body)
117 else:
117 else:
118 expected = 'repository group `%s` does not exist' % (
118 expected = 'repository group `%s` does not exist' % (
119 repo_group.name, )
119 repo_group.name, )
120 assert_error(id_, expected, given=response.body)
120 assert_error(id_, expected, given=response.body)
121
121
122 def test_api_grant_user_permission_to_repo_group_wrong_permission(
122 def test_api_grant_user_permission_to_repo_group_wrong_permission(
123 self, user_util):
123 self, user_util):
124 user = user_util.create_user()
124 user = user_util.create_user()
125 repo_group = user_util.create_repo_group()
125 repo_group = user_util.create_repo_group()
126 perm = 'haha.no.permission'
126 perm = 'haha.no.permission'
127 id_, params = build_data(
127 id_, params = build_data(
128 self.apikey,
128 self.apikey,
129 'grant_user_permission_to_repo_group',
129 'grant_user_permission_to_repo_group',
130 repogroupid=repo_group.name,
130 repogroupid=repo_group.name,
131 userid=user.username,
131 userid=user.username,
132 perm=perm)
132 perm=perm)
133 response = api_call(self.app, params)
133 response = api_call(self.app, params)
134
134
135 expected = 'permission `%s` does not exist' % (perm,)
135 expected = 'permission `%s` does not exist. Permission should start with prefix: `group.`' % (perm,)
136 assert_error(id_, expected, given=response.body)
136 assert_error(id_, expected, given=response.body)
137
137
138 @mock.patch.object(RepoGroupModel, 'grant_user_permission', crash)
138 @mock.patch.object(RepoGroupModel, 'grant_user_permission', crash)
139 def test_api_grant_user_permission_to_repo_group_exception_when_adding(
139 def test_api_grant_user_permission_to_repo_group_exception_when_adding(
140 self, user_util):
140 self, user_util):
141 user = user_util.create_user()
141 user = user_util.create_user()
142 repo_group = user_util.create_repo_group()
142 repo_group = user_util.create_repo_group()
143 perm = 'group.read'
143 perm = 'group.read'
144 id_, params = build_data(
144 id_, params = build_data(
145 self.apikey,
145 self.apikey,
146 'grant_user_permission_to_repo_group',
146 'grant_user_permission_to_repo_group',
147 repogroupid=repo_group.name,
147 repogroupid=repo_group.name,
148 userid=user.username,
148 userid=user.username,
149 perm=perm)
149 perm=perm)
150 response = api_call(self.app, params)
150 response = api_call(self.app, params)
151
151
152 expected = (
152 expected = (
153 'failed to edit permission for user: `%s` in repo group: `%s`' % (
153 'failed to edit permission for user: `%s` in repo group: `%s`' % (
154 user.username, repo_group.name
154 user.username, repo_group.name
155 )
155 )
156 )
156 )
157 assert_error(id_, expected, given=response.body)
157 assert_error(id_, expected, given=response.body)
@@ -1,156 +1,156 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.user import UserModel
24 from rhodecode.model.user import UserModel
25 from rhodecode.model.user_group import UserGroupModel
25 from rhodecode.model.user_group import UserGroupModel
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok, crash)
27 build_data, api_call, assert_error, assert_ok, crash)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestGrantUserPermissionFromUserGroup(object):
31 class TestGrantUserPermissionFromUserGroup(object):
32 @pytest.mark.parametrize("name, perm", [
32 @pytest.mark.parametrize("name, perm", [
33 ('none', 'usergroup.none'),
33 ('none', 'usergroup.none'),
34 ('read', 'usergroup.read'),
34 ('read', 'usergroup.read'),
35 ('write', 'usergroup.write'),
35 ('write', 'usergroup.write'),
36 ('admin', 'usergroup.admin'),
36 ('admin', 'usergroup.admin'),
37
37
38 ('none', 'usergroup.none'),
38 ('none', 'usergroup.none'),
39 ('read', 'usergroup.read'),
39 ('read', 'usergroup.read'),
40 ('write', 'usergroup.write'),
40 ('write', 'usergroup.write'),
41 ('admin', 'usergroup.admin'),
41 ('admin', 'usergroup.admin'),
42
42
43 ('none', 'usergroup.none'),
43 ('none', 'usergroup.none'),
44 ('read', 'usergroup.read'),
44 ('read', 'usergroup.read'),
45 ('write', 'usergroup.write'),
45 ('write', 'usergroup.write'),
46 ('admin', 'usergroup.admin'),
46 ('admin', 'usergroup.admin'),
47
47
48 ('none', 'usergroup.none'),
48 ('none', 'usergroup.none'),
49 ('read', 'usergroup.read'),
49 ('read', 'usergroup.read'),
50 ('write', 'usergroup.write'),
50 ('write', 'usergroup.write'),
51 ('admin', 'usergroup.admin'),
51 ('admin', 'usergroup.admin'),
52 ])
52 ])
53 def test_api_grant_user_permission_to_user_group(
53 def test_api_grant_user_permission_to_user_group(
54 self, name, perm, user_util):
54 self, name, perm, user_util):
55 user = user_util.create_user()
55 user = user_util.create_user()
56 group = user_util.create_user_group()
56 group = user_util.create_user_group()
57 id_, params = build_data(
57 id_, params = build_data(
58 self.apikey,
58 self.apikey,
59 'grant_user_permission_to_user_group',
59 'grant_user_permission_to_user_group',
60 usergroupid=group.users_group_name,
60 usergroupid=group.users_group_name,
61 userid=user.username,
61 userid=user.username,
62 perm=perm)
62 perm=perm)
63 response = api_call(self.app, params)
63 response = api_call(self.app, params)
64
64
65 ret = {
65 ret = {
66 'msg': 'Granted perm: `%s` for user: `%s` in user group: `%s`' % (
66 'msg': 'Granted perm: `%s` for user: `%s` in user group: `%s`' % (
67 perm, user.username, group.users_group_name
67 perm, user.username, group.users_group_name
68 ),
68 ),
69 'success': True
69 'success': True
70 }
70 }
71 expected = ret
71 expected = ret
72 assert_ok(id_, expected, given=response.body)
72 assert_ok(id_, expected, given=response.body)
73
73
74 @pytest.mark.parametrize("name, perm, grant_admin, access_ok", [
74 @pytest.mark.parametrize("name, perm, grant_admin, access_ok", [
75 ('none_fails', 'usergroup.none', False, False),
75 ('none_fails', 'usergroup.none', False, False),
76 ('read_fails', 'usergroup.read', False, False),
76 ('read_fails', 'usergroup.read', False, False),
77 ('write_fails', 'usergroup.write', False, False),
77 ('write_fails', 'usergroup.write', False, False),
78 ('admin_fails', 'usergroup.admin', False, False),
78 ('admin_fails', 'usergroup.admin', False, False),
79
79
80 # with granted perms
80 # with granted perms
81 ('none_ok', 'usergroup.none', True, True),
81 ('none_ok', 'usergroup.none', True, True),
82 ('read_ok', 'usergroup.read', True, True),
82 ('read_ok', 'usergroup.read', True, True),
83 ('write_ok', 'usergroup.write', True, True),
83 ('write_ok', 'usergroup.write', True, True),
84 ('admin_ok', 'usergroup.admin', True, True),
84 ('admin_ok', 'usergroup.admin', True, True),
85 ])
85 ])
86 def test_api_grant_user_permission_to_user_group_by_regular_user(
86 def test_api_grant_user_permission_to_user_group_by_regular_user(
87 self, name, perm, grant_admin, access_ok, user_util):
87 self, name, perm, grant_admin, access_ok, user_util):
88 api_user = UserModel().get_by_username(self.TEST_USER_LOGIN)
88 api_user = UserModel().get_by_username(self.TEST_USER_LOGIN)
89 user = user_util.create_user()
89 user = user_util.create_user()
90 group = user_util.create_user_group()
90 group = user_util.create_user_group()
91 # grant the user ability to at least read the group
91 # grant the user ability to at least read the group
92 permission = 'usergroup.admin' if grant_admin else 'usergroup.read'
92 permission = 'usergroup.admin' if grant_admin else 'usergroup.read'
93 user_util.grant_user_permission_to_user_group(
93 user_util.grant_user_permission_to_user_group(
94 group, api_user, permission)
94 group, api_user, permission)
95
95
96 id_, params = build_data(
96 id_, params = build_data(
97 self.apikey_regular,
97 self.apikey_regular,
98 'grant_user_permission_to_user_group',
98 'grant_user_permission_to_user_group',
99 usergroupid=group.users_group_name,
99 usergroupid=group.users_group_name,
100 userid=user.username,
100 userid=user.username,
101 perm=perm)
101 perm=perm)
102 response = api_call(self.app, params)
102 response = api_call(self.app, params)
103
103
104 if access_ok:
104 if access_ok:
105 ret = {
105 ret = {
106 'msg': (
106 'msg': (
107 'Granted perm: `%s` for user: `%s` in user group: `%s`' % (
107 'Granted perm: `%s` for user: `%s` in user group: `%s`' % (
108 perm, user.username, group.users_group_name
108 perm, user.username, group.users_group_name
109 )
109 )
110 ),
110 ),
111 'success': True
111 'success': True
112 }
112 }
113 expected = ret
113 expected = ret
114 assert_ok(id_, expected, given=response.body)
114 assert_ok(id_, expected, given=response.body)
115 else:
115 else:
116 expected = 'user group `%s` does not exist' % (
116 expected = 'user group `%s` does not exist' % (
117 group.users_group_name)
117 group.users_group_name)
118 assert_error(id_, expected, given=response.body)
118 assert_error(id_, expected, given=response.body)
119
119
120 def test_api_grant_user_permission_to_user_group_wrong_permission(
120 def test_api_grant_user_permission_to_user_group_wrong_permission(
121 self, user_util):
121 self, user_util):
122 user = user_util.create_user()
122 user = user_util.create_user()
123 group = user_util.create_user_group()
123 group = user_util.create_user_group()
124 perm = 'haha.no.permission'
124 perm = 'haha.no.permission'
125 id_, params = build_data(
125 id_, params = build_data(
126 self.apikey,
126 self.apikey,
127 'grant_user_permission_to_user_group',
127 'grant_user_permission_to_user_group',
128 usergroupid=group.users_group_name,
128 usergroupid=group.users_group_name,
129 userid=user.username,
129 userid=user.username,
130 perm=perm)
130 perm=perm)
131 response = api_call(self.app, params)
131 response = api_call(self.app, params)
132
132
133 expected = 'permission `%s` does not exist' % perm
133 expected = 'permission `%s` does not exist. Permission should start with prefix: `usergroup.`' % perm
134 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
135
135
136 def test_api_grant_user_permission_to_user_group_exception_when_adding(
136 def test_api_grant_user_permission_to_user_group_exception_when_adding(
137 self, user_util):
137 self, user_util):
138 user = user_util.create_user()
138 user = user_util.create_user()
139 group = user_util.create_user_group()
139 group = user_util.create_user_group()
140
140
141 perm = 'usergroup.read'
141 perm = 'usergroup.read'
142 id_, params = build_data(
142 id_, params = build_data(
143 self.apikey,
143 self.apikey,
144 'grant_user_permission_to_user_group',
144 'grant_user_permission_to_user_group',
145 usergroupid=group.users_group_name,
145 usergroupid=group.users_group_name,
146 userid=user.username,
146 userid=user.username,
147 perm=perm)
147 perm=perm)
148 with mock.patch.object(UserGroupModel, 'grant_user_permission', crash):
148 with mock.patch.object(UserGroupModel, 'grant_user_permission', crash):
149 response = api_call(self.app, params)
149 response = api_call(self.app, params)
150
150
151 expected = (
151 expected = (
152 'failed to edit permission for user: `%s` in user group: `%s`' % (
152 'failed to edit permission for user: `%s` in user group: `%s`' % (
153 user.username, group.users_group_name
153 user.username, group.users_group_name
154 )
154 )
155 )
155 )
156 assert_error(id_, expected, given=response.body)
156 assert_error(id_, expected, given=response.body)
@@ -1,449 +1,453 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 JSON RPC utils
22 JSON RPC utils
23 """
23 """
24
24
25 import collections
25 import collections
26 import logging
26 import logging
27
27
28 from rhodecode.api.exc import JSONRPCError
28 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
31 from rhodecode.lib.utils import safe_unicode
31 from rhodecode.lib.utils import safe_unicode
32 from rhodecode.lib.vcs.exceptions import RepositoryError
32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.lib.view_utils import get_commit_from_ref_name
33 from rhodecode.lib.view_utils import get_commit_from_ref_name
34 from rhodecode.lib.utils2 import str2bool
34 from rhodecode.lib.utils2 import str2bool
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class OAttr(object):
39 class OAttr(object):
40 """
40 """
41 Special Option that defines other attribute, and can default to them
41 Special Option that defines other attribute, and can default to them
42
42
43 Example::
43 Example::
44
44
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
46 user = Optional.extract(userid, evaluate_locals=local())
46 user = Optional.extract(userid, evaluate_locals=local())
47 #if we pass in userid, we get it, else it will default to apiuser
47 #if we pass in userid, we get it, else it will default to apiuser
48 #attribute
48 #attribute
49 """
49 """
50
50
51 def __init__(self, attr_name):
51 def __init__(self, attr_name):
52 self.attr_name = attr_name
52 self.attr_name = attr_name
53
53
54 def __repr__(self):
54 def __repr__(self):
55 return '<OptionalAttr:%s>' % self.attr_name
55 return '<OptionalAttr:%s>' % self.attr_name
56
56
57 def __call__(self):
57 def __call__(self):
58 return self
58 return self
59
59
60
60
61 class Optional(object):
61 class Optional(object):
62 """
62 """
63 Defines an optional parameter::
63 Defines an optional parameter::
64
64
65 param = param.getval() if isinstance(param, Optional) else param
65 param = param.getval() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
67
67
68 is equivalent of::
68 is equivalent of::
69
69
70 param = Optional.extract(param)
70 param = Optional.extract(param)
71
71
72 """
72 """
73
73
74 def __init__(self, type_):
74 def __init__(self, type_):
75 self.type_ = type_
75 self.type_ = type_
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return '<Optional:%s>' % self.type_.__repr__()
78 return '<Optional:%s>' % self.type_.__repr__()
79
79
80 def __call__(self):
80 def __call__(self):
81 return self.getval()
81 return self.getval()
82
82
83 def getval(self, evaluate_locals=None):
83 def getval(self, evaluate_locals=None):
84 """
84 """
85 returns value from this Optional instance
85 returns value from this Optional instance
86 """
86 """
87 if isinstance(self.type_, OAttr):
87 if isinstance(self.type_, OAttr):
88 param_name = self.type_.attr_name
88 param_name = self.type_.attr_name
89 if evaluate_locals:
89 if evaluate_locals:
90 return evaluate_locals[param_name]
90 return evaluate_locals[param_name]
91 # use params name
91 # use params name
92 return param_name
92 return param_name
93 return self.type_
93 return self.type_
94
94
95 @classmethod
95 @classmethod
96 def extract(cls, val, evaluate_locals=None, binary=None):
96 def extract(cls, val, evaluate_locals=None, binary=None):
97 """
97 """
98 Extracts value from Optional() instance
98 Extracts value from Optional() instance
99
99
100 :param val:
100 :param val:
101 :return: original value if it's not Optional instance else
101 :return: original value if it's not Optional instance else
102 value of instance
102 value of instance
103 """
103 """
104 if isinstance(val, cls):
104 if isinstance(val, cls):
105 val = val.getval(evaluate_locals)
105 val = val.getval(evaluate_locals)
106
106
107 if binary:
107 if binary:
108 val = str2bool(val)
108 val = str2bool(val)
109
109
110 return val
110 return val
111
111
112
112
113 def parse_args(cli_args, key_prefix=''):
113 def parse_args(cli_args, key_prefix=''):
114 from rhodecode.lib.utils2 import (escape_split)
114 from rhodecode.lib.utils2 import (escape_split)
115 kwargs = collections.defaultdict(dict)
115 kwargs = collections.defaultdict(dict)
116 for el in escape_split(cli_args, ','):
116 for el in escape_split(cli_args, ','):
117 kv = escape_split(el, '=', 1)
117 kv = escape_split(el, '=', 1)
118 if len(kv) == 2:
118 if len(kv) == 2:
119 k, v = kv
119 k, v = kv
120 kwargs[key_prefix + k] = v
120 kwargs[key_prefix + k] = v
121 return kwargs
121 return kwargs
122
122
123
123
124 def get_origin(obj):
124 def get_origin(obj):
125 """
125 """
126 Get origin of permission from object.
126 Get origin of permission from object.
127
127
128 :param obj:
128 :param obj:
129 """
129 """
130 origin = 'permission'
130 origin = 'permission'
131
131
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
133 # admin and owner case, maybe we should use dual string ?
133 # admin and owner case, maybe we should use dual string ?
134 origin = 'owner'
134 origin = 'owner'
135 elif getattr(obj, 'owner_row', ''):
135 elif getattr(obj, 'owner_row', ''):
136 origin = 'owner'
136 origin = 'owner'
137 elif getattr(obj, 'admin_row', ''):
137 elif getattr(obj, 'admin_row', ''):
138 origin = 'super-admin'
138 origin = 'super-admin'
139 return origin
139 return origin
140
140
141
141
142 def store_update(updates, attr, name):
142 def store_update(updates, attr, name):
143 """
143 """
144 Stores param in updates dict if it's not instance of Optional
144 Stores param in updates dict if it's not instance of Optional
145 allows easy updates of passed in params
145 allows easy updates of passed in params
146 """
146 """
147 if not isinstance(attr, Optional):
147 if not isinstance(attr, Optional):
148 updates[name] = attr
148 updates[name] = attr
149
149
150
150
151 def has_superadmin_permission(apiuser):
151 def has_superadmin_permission(apiuser):
152 """
152 """
153 Return True if apiuser is admin or return False
153 Return True if apiuser is admin or return False
154
154
155 :param apiuser:
155 :param apiuser:
156 """
156 """
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
158 return True
158 return True
159 return False
159 return False
160
160
161
161
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
163 """
163 """
164 Raise JsonRPCError if apiuser is not authorized or return True
164 Raise JsonRPCError if apiuser is not authorized or return True
165
165
166 :param apiuser:
166 :param apiuser:
167 :param repoid:
167 :param repoid:
168 :param repo:
168 :param repo:
169 :param perms:
169 :param perms:
170 """
170 """
171 if not HasRepoPermissionAnyApi(*perms)(
171 if not HasRepoPermissionAnyApi(*perms)(
172 user=apiuser, repo_name=repo.repo_name):
172 user=apiuser, repo_name=repo.repo_name):
173 raise JSONRPCError(
173 raise JSONRPCError(
174 'repository `%s` does not exist' % repoid)
174 'repository `%s` does not exist' % repoid)
175
175
176 return True
176 return True
177
177
178
178
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
180 """
180 """
181 Raise JsonRPCError if apiuser is not authorized or return True
181 Raise JsonRPCError if apiuser is not authorized or return True
182
182
183 :param apiuser:
183 :param apiuser:
184 :param repogroupid: just the id of repository group
184 :param repogroupid: just the id of repository group
185 :param repo_group: instance of repo_group
185 :param repo_group: instance of repo_group
186 :param perms:
186 :param perms:
187 """
187 """
188 if not HasRepoGroupPermissionAnyApi(*perms)(
188 if not HasRepoGroupPermissionAnyApi(*perms)(
189 user=apiuser, group_name=repo_group.group_name):
189 user=apiuser, group_name=repo_group.group_name):
190 raise JSONRPCError(
190 raise JSONRPCError(
191 'repository group `%s` does not exist' % repogroupid)
191 'repository group `%s` does not exist' % repogroupid)
192
192
193 return True
193 return True
194
194
195
195
196 def validate_set_owner_permissions(apiuser, owner):
196 def validate_set_owner_permissions(apiuser, owner):
197 if isinstance(owner, Optional):
197 if isinstance(owner, Optional):
198 owner = get_user_or_error(apiuser.user_id)
198 owner = get_user_or_error(apiuser.user_id)
199 else:
199 else:
200 if has_superadmin_permission(apiuser):
200 if has_superadmin_permission(apiuser):
201 owner = get_user_or_error(owner)
201 owner = get_user_or_error(owner)
202 else:
202 else:
203 # forbid setting owner for non-admins
203 # forbid setting owner for non-admins
204 raise JSONRPCError(
204 raise JSONRPCError(
205 'Only RhodeCode super-admin can specify `owner` param')
205 'Only RhodeCode super-admin can specify `owner` param')
206 return owner
206 return owner
207
207
208
208
209 def get_user_or_error(userid):
209 def get_user_or_error(userid):
210 """
210 """
211 Get user by id or name or return JsonRPCError if not found
211 Get user by id or name or return JsonRPCError if not found
212
212
213 :param userid:
213 :param userid:
214 """
214 """
215 from rhodecode.model.user import UserModel
215 from rhodecode.model.user import UserModel
216 user_model = UserModel()
216 user_model = UserModel()
217
217
218 if isinstance(userid, (int, long)):
218 if isinstance(userid, (int, long)):
219 try:
219 try:
220 user = user_model.get_user(userid)
220 user = user_model.get_user(userid)
221 except ValueError:
221 except ValueError:
222 user = None
222 user = None
223 else:
223 else:
224 user = user_model.get_by_username(userid)
224 user = user_model.get_by_username(userid)
225
225
226 if user is None:
226 if user is None:
227 raise JSONRPCError(
227 raise JSONRPCError(
228 'user `%s` does not exist' % (userid,))
228 'user `%s` does not exist' % (userid,))
229 return user
229 return user
230
230
231
231
232 def get_repo_or_error(repoid):
232 def get_repo_or_error(repoid):
233 """
233 """
234 Get repo by id or name or return JsonRPCError if not found
234 Get repo by id or name or return JsonRPCError if not found
235
235
236 :param repoid:
236 :param repoid:
237 """
237 """
238 from rhodecode.model.repo import RepoModel
238 from rhodecode.model.repo import RepoModel
239 repo_model = RepoModel()
239 repo_model = RepoModel()
240
240
241 if isinstance(repoid, (int, long)):
241 if isinstance(repoid, (int, long)):
242 try:
242 try:
243 repo = repo_model.get_repo(repoid)
243 repo = repo_model.get_repo(repoid)
244 except ValueError:
244 except ValueError:
245 repo = None
245 repo = None
246 else:
246 else:
247 repo = repo_model.get_by_repo_name(repoid)
247 repo = repo_model.get_by_repo_name(repoid)
248
248
249 if repo is None:
249 if repo is None:
250 raise JSONRPCError(
250 raise JSONRPCError(
251 'repository `%s` does not exist' % (repoid,))
251 'repository `%s` does not exist' % (repoid,))
252 return repo
252 return repo
253
253
254
254
255 def get_repo_group_or_error(repogroupid):
255 def get_repo_group_or_error(repogroupid):
256 """
256 """
257 Get repo group by id or name or return JsonRPCError if not found
257 Get repo group by id or name or return JsonRPCError if not found
258
258
259 :param repogroupid:
259 :param repogroupid:
260 """
260 """
261 from rhodecode.model.repo_group import RepoGroupModel
261 from rhodecode.model.repo_group import RepoGroupModel
262 repo_group_model = RepoGroupModel()
262 repo_group_model = RepoGroupModel()
263
263
264 if isinstance(repogroupid, (int, long)):
264 if isinstance(repogroupid, (int, long)):
265 try:
265 try:
266 repo_group = repo_group_model._get_repo_group(repogroupid)
266 repo_group = repo_group_model._get_repo_group(repogroupid)
267 except ValueError:
267 except ValueError:
268 repo_group = None
268 repo_group = None
269 else:
269 else:
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
271
271
272 if repo_group is None:
272 if repo_group is None:
273 raise JSONRPCError(
273 raise JSONRPCError(
274 'repository group `%s` does not exist' % (repogroupid,))
274 'repository group `%s` does not exist' % (repogroupid,))
275 return repo_group
275 return repo_group
276
276
277
277
278 def get_user_group_or_error(usergroupid):
278 def get_user_group_or_error(usergroupid):
279 """
279 """
280 Get user group by id or name or return JsonRPCError if not found
280 Get user group by id or name or return JsonRPCError if not found
281
281
282 :param usergroupid:
282 :param usergroupid:
283 """
283 """
284 from rhodecode.model.user_group import UserGroupModel
284 from rhodecode.model.user_group import UserGroupModel
285 user_group_model = UserGroupModel()
285 user_group_model = UserGroupModel()
286
286
287 if isinstance(usergroupid, (int, long)):
287 if isinstance(usergroupid, (int, long)):
288 try:
288 try:
289 user_group = user_group_model.get_group(usergroupid)
289 user_group = user_group_model.get_group(usergroupid)
290 except ValueError:
290 except ValueError:
291 user_group = None
291 user_group = None
292 else:
292 else:
293 user_group = user_group_model.get_by_name(usergroupid)
293 user_group = user_group_model.get_by_name(usergroupid)
294
294
295 if user_group is None:
295 if user_group is None:
296 raise JSONRPCError(
296 raise JSONRPCError(
297 'user group `%s` does not exist' % (usergroupid,))
297 'user group `%s` does not exist' % (usergroupid,))
298 return user_group
298 return user_group
299
299
300
300
301 def get_perm_or_error(permid, prefix=None):
301 def get_perm_or_error(permid, prefix=None):
302 """
302 """
303 Get permission by id or name or return JsonRPCError if not found
303 Get permission by id or name or return JsonRPCError if not found
304
304
305 :param permid:
305 :param permid:
306 """
306 """
307 from rhodecode.model.permission import PermissionModel
307 from rhodecode.model.permission import PermissionModel
308
308
309 perm = PermissionModel.cls.get_by_key(permid)
309 perm = PermissionModel.cls.get_by_key(permid)
310 if perm is None:
310 if perm is None:
311 raise JSONRPCError('permission `%s` does not exist' % (permid,))
311 msg = 'permission `{}` does not exist.'.format(permid)
312 if prefix:
313 msg += ' Permission should start with prefix: `{}`'.format(prefix)
314 raise JSONRPCError(msg)
315
312 if prefix:
316 if prefix:
313 if not perm.permission_name.startswith(prefix):
317 if not perm.permission_name.startswith(prefix):
314 raise JSONRPCError('permission `%s` is invalid, '
318 raise JSONRPCError('permission `%s` is invalid, '
315 'should start with %s' % (permid, prefix))
319 'should start with %s' % (permid, prefix))
316 return perm
320 return perm
317
321
318
322
319 def get_gist_or_error(gistid):
323 def get_gist_or_error(gistid):
320 """
324 """
321 Get gist by id or gist_access_id or return JsonRPCError if not found
325 Get gist by id or gist_access_id or return JsonRPCError if not found
322
326
323 :param gistid:
327 :param gistid:
324 """
328 """
325 from rhodecode.model.gist import GistModel
329 from rhodecode.model.gist import GistModel
326
330
327 gist = GistModel.cls.get_by_access_id(gistid)
331 gist = GistModel.cls.get_by_access_id(gistid)
328 if gist is None:
332 if gist is None:
329 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
333 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
330 return gist
334 return gist
331
335
332
336
333 def get_pull_request_or_error(pullrequestid):
337 def get_pull_request_or_error(pullrequestid):
334 """
338 """
335 Get pull request by id or return JsonRPCError if not found
339 Get pull request by id or return JsonRPCError if not found
336
340
337 :param pullrequestid:
341 :param pullrequestid:
338 """
342 """
339 from rhodecode.model.pull_request import PullRequestModel
343 from rhodecode.model.pull_request import PullRequestModel
340
344
341 try:
345 try:
342 pull_request = PullRequestModel().get(int(pullrequestid))
346 pull_request = PullRequestModel().get(int(pullrequestid))
343 except ValueError:
347 except ValueError:
344 raise JSONRPCError('pullrequestid must be an integer')
348 raise JSONRPCError('pullrequestid must be an integer')
345 if not pull_request:
349 if not pull_request:
346 raise JSONRPCError('pull request `%s` does not exist' % (
350 raise JSONRPCError('pull request `%s` does not exist' % (
347 pullrequestid,))
351 pullrequestid,))
348 return pull_request
352 return pull_request
349
353
350
354
351 def build_commit_data(commit, detail_level):
355 def build_commit_data(commit, detail_level):
352 parsed_diff = []
356 parsed_diff = []
353 if detail_level == 'extended':
357 if detail_level == 'extended':
354 for f in commit.added:
358 for f_path in commit.added_paths:
355 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
359 parsed_diff.append(_get_commit_dict(filename=f_path, op='A'))
356 for f in commit.changed:
360 for f_path in commit.changed_paths:
357 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
361 parsed_diff.append(_get_commit_dict(filename=f_path, op='M'))
358 for f in commit.removed:
362 for f_path in commit.removed_paths:
359 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
363 parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
360
364
361 elif detail_level == 'full':
365 elif detail_level == 'full':
362 from rhodecode.lib.diffs import DiffProcessor
366 from rhodecode.lib.diffs import DiffProcessor
363 diff_processor = DiffProcessor(commit.diff())
367 diff_processor = DiffProcessor(commit.diff())
364 for dp in diff_processor.prepare():
368 for dp in diff_processor.prepare():
365 del dp['stats']['ops']
369 del dp['stats']['ops']
366 _stats = dp['stats']
370 _stats = dp['stats']
367 parsed_diff.append(_get_commit_dict(
371 parsed_diff.append(_get_commit_dict(
368 filename=dp['filename'], op=dp['operation'],
372 filename=dp['filename'], op=dp['operation'],
369 new_revision=dp['new_revision'],
373 new_revision=dp['new_revision'],
370 old_revision=dp['old_revision'],
374 old_revision=dp['old_revision'],
371 raw_diff=dp['raw_diff'], stats=_stats))
375 raw_diff=dp['raw_diff'], stats=_stats))
372
376
373 return parsed_diff
377 return parsed_diff
374
378
375
379
376 def get_commit_or_error(ref, repo):
380 def get_commit_or_error(ref, repo):
377 try:
381 try:
378 ref_type, _, ref_hash = ref.split(':')
382 ref_type, _, ref_hash = ref.split(':')
379 except ValueError:
383 except ValueError:
380 raise JSONRPCError(
384 raise JSONRPCError(
381 'Ref `{ref}` given in a wrong format. Please check the API'
385 'Ref `{ref}` given in a wrong format. Please check the API'
382 ' documentation for more details'.format(ref=ref))
386 ' documentation for more details'.format(ref=ref))
383 try:
387 try:
384 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
388 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
385 # once get_commit supports ref_types
389 # once get_commit supports ref_types
386 return get_commit_from_ref_name(repo, ref_hash)
390 return get_commit_from_ref_name(repo, ref_hash)
387 except RepositoryError:
391 except RepositoryError:
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
392 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
389
393
390
394
391 def _get_ref_hash(repo, type_, name):
395 def _get_ref_hash(repo, type_, name):
392 vcs_repo = repo.scm_instance()
396 vcs_repo = repo.scm_instance()
393 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
397 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
394 return vcs_repo.branches[name]
398 return vcs_repo.branches[name]
395 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
399 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
396 return vcs_repo.bookmarks[name]
400 return vcs_repo.bookmarks[name]
397 else:
401 else:
398 raise ValueError()
402 raise ValueError()
399
403
400
404
401 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
405 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
402 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
406 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
403
407
404 def _parse_ref(type_, name, hash_=None):
408 def _parse_ref(type_, name, hash_=None):
405 return type_, name, hash_
409 return type_, name, hash_
406
410
407 try:
411 try:
408 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
412 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
409 except TypeError:
413 except TypeError:
410 raise JSONRPCError(
414 raise JSONRPCError(
411 'Ref `{ref}` given in a wrong format. Please check the API'
415 'Ref `{ref}` given in a wrong format. Please check the API'
412 ' documentation for more details'.format(ref=ref))
416 ' documentation for more details'.format(ref=ref))
413
417
414 if ref_type not in allowed_ref_types:
418 if ref_type not in allowed_ref_types:
415 raise JSONRPCError(
419 raise JSONRPCError(
416 'Ref `{ref}` type is not allowed. '
420 'Ref `{ref}` type is not allowed. '
417 'Only:{allowed_refs} are possible.'.format(
421 'Only:{allowed_refs} are possible.'.format(
418 ref=ref, allowed_refs=allowed_ref_types))
422 ref=ref, allowed_refs=allowed_ref_types))
419
423
420 try:
424 try:
421 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
425 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
422 except (KeyError, ValueError):
426 except (KeyError, ValueError):
423 raise JSONRPCError(
427 raise JSONRPCError(
424 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
428 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
425 type=ref_type, name=ref_name))
429 type=ref_type, name=ref_name))
426
430
427 return ':'.join([ref_type, ref_name, ref_hash])
431 return ':'.join([ref_type, ref_name, ref_hash])
428
432
429
433
430 def _get_commit_dict(
434 def _get_commit_dict(
431 filename, op, new_revision=None, old_revision=None,
435 filename, op, new_revision=None, old_revision=None,
432 raw_diff=None, stats=None):
436 raw_diff=None, stats=None):
433 if stats is None:
437 if stats is None:
434 stats = {
438 stats = {
435 "added": None,
439 "added": None,
436 "binary": None,
440 "binary": None,
437 "deleted": None
441 "deleted": None
438 }
442 }
439 return {
443 return {
440 "filename": safe_unicode(filename),
444 "filename": safe_unicode(filename),
441 "op": op,
445 "op": op,
442
446
443 # extra details
447 # extra details
444 "new_revision": new_revision,
448 "new_revision": new_revision,
445 "old_revision": old_revision,
449 "old_revision": old_revision,
446
450
447 "raw_diff": raw_diff,
451 "raw_diff": raw_diff,
448 "stats": stats
452 "stats": stats
449 }
453 }
@@ -1,1011 +1,1016 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 merge_state=Optional(False)):
47 merge_state=Optional(False)):
48 """
48 """
49 Get a pull request based on the given ID.
49 Get a pull request based on the given ID.
50
50
51 :param apiuser: This is filled automatically from the |authtoken|.
51 :param apiuser: This is filled automatically from the |authtoken|.
52 :type apiuser: AuthUser
52 :type apiuser: AuthUser
53 :param repoid: Optional, repository name or repository ID from where
53 :param repoid: Optional, repository name or repository ID from where
54 the pull request was opened.
54 the pull request was opened.
55 :type repoid: str or int
55 :type repoid: str or int
56 :param pullrequestid: ID of the requested pull request.
56 :param pullrequestid: ID of the requested pull request.
57 :type pullrequestid: int
57 :type pullrequestid: int
58 :param merge_state: Optional calculate merge state for each repository.
58 :param merge_state: Optional calculate merge state for each repository.
59 This could result in longer time to fetch the data
59 This could result in longer time to fetch the data
60 :type merge_state: bool
60 :type merge_state: bool
61
61
62 Example output:
62 Example output:
63
63
64 .. code-block:: bash
64 .. code-block:: bash
65
65
66 "id": <id_given_in_input>,
66 "id": <id_given_in_input>,
67 "result":
67 "result":
68 {
68 {
69 "pull_request_id": "<pull_request_id>",
69 "pull_request_id": "<pull_request_id>",
70 "url": "<url>",
70 "url": "<url>",
71 "title": "<title>",
71 "title": "<title>",
72 "description": "<description>",
72 "description": "<description>",
73 "status" : "<status>",
73 "status" : "<status>",
74 "created_on": "<date_time_created>",
74 "created_on": "<date_time_created>",
75 "updated_on": "<date_time_updated>",
75 "updated_on": "<date_time_updated>",
76 "versions": "<number_or_versions_of_pr>",
76 "commit_ids": [
77 "commit_ids": [
77 ...
78 ...
78 "<commit_id>",
79 "<commit_id>",
79 "<commit_id>",
80 "<commit_id>",
80 ...
81 ...
81 ],
82 ],
82 "review_status": "<review_status>",
83 "review_status": "<review_status>",
83 "mergeable": {
84 "mergeable": {
84 "status": "<bool>",
85 "status": "<bool>",
85 "message": "<message>",
86 "message": "<message>",
86 },
87 },
87 "source": {
88 "source": {
88 "clone_url": "<clone_url>",
89 "clone_url": "<clone_url>",
89 "repository": "<repository_name>",
90 "repository": "<repository_name>",
90 "reference":
91 "reference":
91 {
92 {
92 "name": "<name>",
93 "name": "<name>",
93 "type": "<type>",
94 "type": "<type>",
94 "commit_id": "<commit_id>",
95 "commit_id": "<commit_id>",
95 }
96 }
96 },
97 },
97 "target": {
98 "target": {
98 "clone_url": "<clone_url>",
99 "clone_url": "<clone_url>",
99 "repository": "<repository_name>",
100 "repository": "<repository_name>",
100 "reference":
101 "reference":
101 {
102 {
102 "name": "<name>",
103 "name": "<name>",
103 "type": "<type>",
104 "type": "<type>",
104 "commit_id": "<commit_id>",
105 "commit_id": "<commit_id>",
105 }
106 }
106 },
107 },
107 "merge": {
108 "merge": {
108 "clone_url": "<clone_url>",
109 "clone_url": "<clone_url>",
109 "reference":
110 "reference":
110 {
111 {
111 "name": "<name>",
112 "name": "<name>",
112 "type": "<type>",
113 "type": "<type>",
113 "commit_id": "<commit_id>",
114 "commit_id": "<commit_id>",
114 }
115 }
115 },
116 },
116 "author": <user_obj>,
117 "author": <user_obj>,
117 "reviewers": [
118 "reviewers": [
118 ...
119 ...
119 {
120 {
120 "user": "<user_obj>",
121 "user": "<user_obj>",
121 "review_status": "<review_status>",
122 "review_status": "<review_status>",
122 }
123 }
123 ...
124 ...
124 ]
125 ]
125 },
126 },
126 "error": null
127 "error": null
127 """
128 """
128
129
129 pull_request = get_pull_request_or_error(pullrequestid)
130 pull_request = get_pull_request_or_error(pullrequestid)
130 if Optional.extract(repoid):
131 if Optional.extract(repoid):
131 repo = get_repo_or_error(repoid)
132 repo = get_repo_or_error(repoid)
132 else:
133 else:
133 repo = pull_request.target_repo
134 repo = pull_request.target_repo
134
135
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 raise JSONRPCError('repository `%s` or pull request `%s` '
137 raise JSONRPCError('repository `%s` or pull request `%s` '
137 'does not exist' % (repoid, pullrequestid))
138 'does not exist' % (repoid, pullrequestid))
138
139
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # is happening.
142 # is happening.
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 data = pull_request.get_api_data(with_merge_state=merge_state)
145 data = pull_request.get_api_data(with_merge_state=merge_state)
145 return data
146 return data
146
147
147
148
148 @jsonrpc_method()
149 @jsonrpc_method()
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 merge_state=Optional(False)):
151 merge_state=Optional(False)):
151 """
152 """
152 Get all pull requests from the repository specified in `repoid`.
153 Get all pull requests from the repository specified in `repoid`.
153
154
154 :param apiuser: This is filled automatically from the |authtoken|.
155 :param apiuser: This is filled automatically from the |authtoken|.
155 :type apiuser: AuthUser
156 :type apiuser: AuthUser
156 :param repoid: Optional repository name or repository ID.
157 :param repoid: Optional repository name or repository ID.
157 :type repoid: str or int
158 :type repoid: str or int
158 :param status: Only return pull requests with the specified status.
159 :param status: Only return pull requests with the specified status.
159 Valid options are.
160 Valid options are.
160 * ``new`` (default)
161 * ``new`` (default)
161 * ``open``
162 * ``open``
162 * ``closed``
163 * ``closed``
163 :type status: str
164 :type status: str
164 :param merge_state: Optional calculate merge state for each repository.
165 :param merge_state: Optional calculate merge state for each repository.
165 This could result in longer time to fetch the data
166 This could result in longer time to fetch the data
166 :type merge_state: bool
167 :type merge_state: bool
167
168
168 Example output:
169 Example output:
169
170
170 .. code-block:: bash
171 .. code-block:: bash
171
172
172 "id": <id_given_in_input>,
173 "id": <id_given_in_input>,
173 "result":
174 "result":
174 [
175 [
175 ...
176 ...
176 {
177 {
177 "pull_request_id": "<pull_request_id>",
178 "pull_request_id": "<pull_request_id>",
178 "url": "<url>",
179 "url": "<url>",
179 "title" : "<title>",
180 "title" : "<title>",
180 "description": "<description>",
181 "description": "<description>",
181 "status": "<status>",
182 "status": "<status>",
182 "created_on": "<date_time_created>",
183 "created_on": "<date_time_created>",
183 "updated_on": "<date_time_updated>",
184 "updated_on": "<date_time_updated>",
184 "commit_ids": [
185 "commit_ids": [
185 ...
186 ...
186 "<commit_id>",
187 "<commit_id>",
187 "<commit_id>",
188 "<commit_id>",
188 ...
189 ...
189 ],
190 ],
190 "review_status": "<review_status>",
191 "review_status": "<review_status>",
191 "mergeable": {
192 "mergeable": {
192 "status": "<bool>",
193 "status": "<bool>",
193 "message: "<message>",
194 "message: "<message>",
194 },
195 },
195 "source": {
196 "source": {
196 "clone_url": "<clone_url>",
197 "clone_url": "<clone_url>",
197 "reference":
198 "reference":
198 {
199 {
199 "name": "<name>",
200 "name": "<name>",
200 "type": "<type>",
201 "type": "<type>",
201 "commit_id": "<commit_id>",
202 "commit_id": "<commit_id>",
202 }
203 }
203 },
204 },
204 "target": {
205 "target": {
205 "clone_url": "<clone_url>",
206 "clone_url": "<clone_url>",
206 "reference":
207 "reference":
207 {
208 {
208 "name": "<name>",
209 "name": "<name>",
209 "type": "<type>",
210 "type": "<type>",
210 "commit_id": "<commit_id>",
211 "commit_id": "<commit_id>",
211 }
212 }
212 },
213 },
213 "merge": {
214 "merge": {
214 "clone_url": "<clone_url>",
215 "clone_url": "<clone_url>",
215 "reference":
216 "reference":
216 {
217 {
217 "name": "<name>",
218 "name": "<name>",
218 "type": "<type>",
219 "type": "<type>",
219 "commit_id": "<commit_id>",
220 "commit_id": "<commit_id>",
220 }
221 }
221 },
222 },
222 "author": <user_obj>,
223 "author": <user_obj>,
223 "reviewers": [
224 "reviewers": [
224 ...
225 ...
225 {
226 {
226 "user": "<user_obj>",
227 "user": "<user_obj>",
227 "review_status": "<review_status>",
228 "review_status": "<review_status>",
228 }
229 }
229 ...
230 ...
230 ]
231 ]
231 }
232 }
232 ...
233 ...
233 ],
234 ],
234 "error": null
235 "error": null
235
236
236 """
237 """
237 repo = get_repo_or_error(repoid)
238 repo = get_repo_or_error(repoid)
238 if not has_superadmin_permission(apiuser):
239 if not has_superadmin_permission(apiuser):
239 _perms = (
240 _perms = (
240 'repository.admin', 'repository.write', 'repository.read',)
241 'repository.admin', 'repository.write', 'repository.read',)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
242 validate_repo_permissions(apiuser, repoid, repo, _perms)
242
243
243 status = Optional.extract(status)
244 status = Optional.extract(status)
244 merge_state = Optional.extract(merge_state, binary=True)
245 merge_state = Optional.extract(merge_state, binary=True)
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 order_by='id', order_dir='desc')
247 order_by='id', order_dir='desc')
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 return data
249 return data
249
250
250
251
251 @jsonrpc_method()
252 @jsonrpc_method()
252 def merge_pull_request(
253 def merge_pull_request(
253 request, apiuser, pullrequestid, repoid=Optional(None),
254 request, apiuser, pullrequestid, repoid=Optional(None),
254 userid=Optional(OAttr('apiuser'))):
255 userid=Optional(OAttr('apiuser'))):
255 """
256 """
256 Merge the pull request specified by `pullrequestid` into its target
257 Merge the pull request specified by `pullrequestid` into its target
257 repository.
258 repository.
258
259
259 :param apiuser: This is filled automatically from the |authtoken|.
260 :param apiuser: This is filled automatically from the |authtoken|.
260 :type apiuser: AuthUser
261 :type apiuser: AuthUser
261 :param repoid: Optional, repository name or repository ID of the
262 :param repoid: Optional, repository name or repository ID of the
262 target repository to which the |pr| is to be merged.
263 target repository to which the |pr| is to be merged.
263 :type repoid: str or int
264 :type repoid: str or int
264 :param pullrequestid: ID of the pull request which shall be merged.
265 :param pullrequestid: ID of the pull request which shall be merged.
265 :type pullrequestid: int
266 :type pullrequestid: int
266 :param userid: Merge the pull request as this user.
267 :param userid: Merge the pull request as this user.
267 :type userid: Optional(str or int)
268 :type userid: Optional(str or int)
268
269
269 Example output:
270 Example output:
270
271
271 .. code-block:: bash
272 .. code-block:: bash
272
273
273 "id": <id_given_in_input>,
274 "id": <id_given_in_input>,
274 "result": {
275 "result": {
275 "executed": "<bool>",
276 "executed": "<bool>",
276 "failure_reason": "<int>",
277 "failure_reason": "<int>",
277 "merge_status_message": "<str>",
278 "merge_status_message": "<str>",
278 "merge_commit_id": "<merge_commit_id>",
279 "merge_commit_id": "<merge_commit_id>",
279 "possible": "<bool>",
280 "possible": "<bool>",
280 "merge_ref": {
281 "merge_ref": {
281 "commit_id": "<commit_id>",
282 "commit_id": "<commit_id>",
282 "type": "<type>",
283 "type": "<type>",
283 "name": "<name>"
284 "name": "<name>"
284 }
285 }
285 },
286 },
286 "error": null
287 "error": null
287 """
288 """
288 pull_request = get_pull_request_or_error(pullrequestid)
289 pull_request = get_pull_request_or_error(pullrequestid)
289 if Optional.extract(repoid):
290 if Optional.extract(repoid):
290 repo = get_repo_or_error(repoid)
291 repo = get_repo_or_error(repoid)
291 else:
292 else:
292 repo = pull_request.target_repo
293 repo = pull_request.target_repo
293 auth_user = apiuser
294 auth_user = apiuser
294 if not isinstance(userid, Optional):
295 if not isinstance(userid, Optional):
295 if (has_superadmin_permission(apiuser) or
296 if (has_superadmin_permission(apiuser) or
296 HasRepoPermissionAnyApi('repository.admin')(
297 HasRepoPermissionAnyApi('repository.admin')(
297 user=apiuser, repo_name=repo.repo_name)):
298 user=apiuser, repo_name=repo.repo_name)):
298 apiuser = get_user_or_error(userid)
299 apiuser = get_user_or_error(userid)
299 auth_user = apiuser.AuthUser()
300 auth_user = apiuser.AuthUser()
300 else:
301 else:
301 raise JSONRPCError('userid is not the same as your user')
302 raise JSONRPCError('userid is not the same as your user')
302
303
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 raise JSONRPCError(
305 raise JSONRPCError(
305 'Operation forbidden because pull request is in state {}, '
306 'Operation forbidden because pull request is in state {}, '
306 'only state {} is allowed.'.format(
307 'only state {} is allowed.'.format(
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308
309
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 translator=request.translate)
312 translator=request.translate)
312 merge_possible = not check.failed
313 merge_possible = not check.failed
313
314
314 if not merge_possible:
315 if not merge_possible:
315 error_messages = []
316 error_messages = []
316 for err_type, error_msg in check.errors:
317 for err_type, error_msg in check.errors:
317 error_msg = request.translate(error_msg)
318 error_msg = request.translate(error_msg)
318 error_messages.append(error_msg)
319 error_messages.append(error_msg)
319
320
320 reasons = ','.join(error_messages)
321 reasons = ','.join(error_messages)
321 raise JSONRPCError(
322 raise JSONRPCError(
322 'merge not possible for following reasons: {}'.format(reasons))
323 'merge not possible for following reasons: {}'.format(reasons))
323
324
324 target_repo = pull_request.target_repo
325 target_repo = pull_request.target_repo
325 extras = vcs_operation_context(
326 extras = vcs_operation_context(
326 request.environ, repo_name=target_repo.repo_name,
327 request.environ, repo_name=target_repo.repo_name,
327 username=auth_user.username, action='push',
328 username=auth_user.username, action='push',
328 scm=target_repo.repo_type)
329 scm=target_repo.repo_type)
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 merge_response = PullRequestModel().merge_repo(
331 merge_response = PullRequestModel().merge_repo(
331 pull_request, apiuser, extras=extras)
332 pull_request, apiuser, extras=extras)
332 if merge_response.executed:
333 if merge_response.executed:
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334
335
335 Session().commit()
336 Session().commit()
336
337
337 # In previous versions the merge response directly contained the merge
338 # In previous versions the merge response directly contained the merge
338 # commit id. It is now contained in the merge reference object. To be
339 # commit id. It is now contained in the merge reference object. To be
339 # backwards compatible we have to extract it again.
340 # backwards compatible we have to extract it again.
340 merge_response = merge_response.asdict()
341 merge_response = merge_response.asdict()
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342
343
343 return merge_response
344 return merge_response
344
345
345
346
346 @jsonrpc_method()
347 @jsonrpc_method()
347 def get_pull_request_comments(
348 def get_pull_request_comments(
348 request, apiuser, pullrequestid, repoid=Optional(None)):
349 request, apiuser, pullrequestid, repoid=Optional(None)):
349 """
350 """
350 Get all comments of pull request specified with the `pullrequestid`
351 Get all comments of pull request specified with the `pullrequestid`
351
352
352 :param apiuser: This is filled automatically from the |authtoken|.
353 :param apiuser: This is filled automatically from the |authtoken|.
353 :type apiuser: AuthUser
354 :type apiuser: AuthUser
354 :param repoid: Optional repository name or repository ID.
355 :param repoid: Optional repository name or repository ID.
355 :type repoid: str or int
356 :type repoid: str or int
356 :param pullrequestid: The pull request ID.
357 :param pullrequestid: The pull request ID.
357 :type pullrequestid: int
358 :type pullrequestid: int
358
359
359 Example output:
360 Example output:
360
361
361 .. code-block:: bash
362 .. code-block:: bash
362
363
363 id : <id_given_in_input>
364 id : <id_given_in_input>
364 result : [
365 result : [
365 {
366 {
366 "comment_author": {
367 "comment_author": {
367 "active": true,
368 "active": true,
368 "full_name_or_username": "Tom Gore",
369 "full_name_or_username": "Tom Gore",
369 "username": "admin"
370 "username": "admin"
370 },
371 },
371 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_f_path": null,
373 "comment_f_path": null,
373 "comment_id": 25,
374 "comment_id": 25,
374 "comment_lineno": null,
375 "comment_lineno": null,
375 "comment_status": {
376 "comment_status": {
376 "status": "under_review",
377 "status": "under_review",
377 "status_lbl": "Under Review"
378 "status_lbl": "Under Review"
378 },
379 },
379 "comment_text": "Example text",
380 "comment_text": "Example text",
380 "comment_type": null,
381 "comment_type": null,
381 "pull_request_version": null
382 "pull_request_version": null
382 }
383 }
383 ],
384 ],
384 error : null
385 error : null
385 """
386 """
386
387
387 pull_request = get_pull_request_or_error(pullrequestid)
388 pull_request = get_pull_request_or_error(pullrequestid)
388 if Optional.extract(repoid):
389 if Optional.extract(repoid):
389 repo = get_repo_or_error(repoid)
390 repo = get_repo_or_error(repoid)
390 else:
391 else:
391 repo = pull_request.target_repo
392 repo = pull_request.target_repo
392
393
393 if not PullRequestModel().check_user_read(
394 if not PullRequestModel().check_user_read(
394 pull_request, apiuser, api=True):
395 pull_request, apiuser, api=True):
395 raise JSONRPCError('repository `%s` or pull request `%s` '
396 raise JSONRPCError('repository `%s` or pull request `%s` '
396 'does not exist' % (repoid, pullrequestid))
397 'does not exist' % (repoid, pullrequestid))
397
398
398 (pull_request_latest,
399 (pull_request_latest,
399 pull_request_at_ver,
400 pull_request_at_ver,
400 pull_request_display_obj,
401 pull_request_display_obj,
401 at_version) = PullRequestModel().get_pr_version(
402 at_version) = PullRequestModel().get_pr_version(
402 pull_request.pull_request_id, version=None)
403 pull_request.pull_request_id, version=None)
403
404
404 versions = pull_request_display_obj.versions()
405 versions = pull_request_display_obj.versions()
405 ver_map = {
406 ver_map = {
406 ver.pull_request_version_id: cnt
407 ver.pull_request_version_id: cnt
407 for cnt, ver in enumerate(versions, 1)
408 for cnt, ver in enumerate(versions, 1)
408 }
409 }
409
410
410 # GENERAL COMMENTS with versions #
411 # GENERAL COMMENTS with versions #
411 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
412 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
412 q = q.order_by(ChangesetComment.comment_id.asc())
413 q = q.order_by(ChangesetComment.comment_id.asc())
413 general_comments = q.all()
414 general_comments = q.all()
414
415
415 # INLINE COMMENTS with versions #
416 # INLINE COMMENTS with versions #
416 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
417 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
417 q = q.order_by(ChangesetComment.comment_id.asc())
418 q = q.order_by(ChangesetComment.comment_id.asc())
418 inline_comments = q.all()
419 inline_comments = q.all()
419
420
420 data = []
421 data = []
421 for comment in inline_comments + general_comments:
422 for comment in inline_comments + general_comments:
422 full_data = comment.get_api_data()
423 full_data = comment.get_api_data()
423 pr_version_id = None
424 pr_version_id = None
424 if comment.pull_request_version_id:
425 if comment.pull_request_version_id:
425 pr_version_id = 'v{}'.format(
426 pr_version_id = 'v{}'.format(
426 ver_map[comment.pull_request_version_id])
427 ver_map[comment.pull_request_version_id])
427
428
428 # sanitize some entries
429 # sanitize some entries
429
430
430 full_data['pull_request_version'] = pr_version_id
431 full_data['pull_request_version'] = pr_version_id
431 full_data['comment_author'] = {
432 full_data['comment_author'] = {
432 'username': full_data['comment_author'].username,
433 'username': full_data['comment_author'].username,
433 'full_name_or_username': full_data['comment_author'].full_name_or_username,
434 'full_name_or_username': full_data['comment_author'].full_name_or_username,
434 'active': full_data['comment_author'].active,
435 'active': full_data['comment_author'].active,
435 }
436 }
436
437
437 if full_data['comment_status']:
438 if full_data['comment_status']:
438 full_data['comment_status'] = {
439 full_data['comment_status'] = {
439 'status': full_data['comment_status'][0].status,
440 'status': full_data['comment_status'][0].status,
440 'status_lbl': full_data['comment_status'][0].status_lbl,
441 'status_lbl': full_data['comment_status'][0].status_lbl,
441 }
442 }
442 else:
443 else:
443 full_data['comment_status'] = {}
444 full_data['comment_status'] = {}
444
445
445 data.append(full_data)
446 data.append(full_data)
446 return data
447 return data
447
448
448
449
449 @jsonrpc_method()
450 @jsonrpc_method()
450 def comment_pull_request(
451 def comment_pull_request(
451 request, apiuser, pullrequestid, repoid=Optional(None),
452 request, apiuser, pullrequestid, repoid=Optional(None),
452 message=Optional(None), commit_id=Optional(None), status=Optional(None),
453 message=Optional(None), commit_id=Optional(None), status=Optional(None),
453 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
454 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
454 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
455 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
455 userid=Optional(OAttr('apiuser'))):
456 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
456 """
457 """
457 Comment on the pull request specified with the `pullrequestid`,
458 Comment on the pull request specified with the `pullrequestid`,
458 in the |repo| specified by the `repoid`, and optionally change the
459 in the |repo| specified by the `repoid`, and optionally change the
459 review status.
460 review status.
460
461
461 :param apiuser: This is filled automatically from the |authtoken|.
462 :param apiuser: This is filled automatically from the |authtoken|.
462 :type apiuser: AuthUser
463 :type apiuser: AuthUser
463 :param repoid: Optional repository name or repository ID.
464 :param repoid: Optional repository name or repository ID.
464 :type repoid: str or int
465 :type repoid: str or int
465 :param pullrequestid: The pull request ID.
466 :param pullrequestid: The pull request ID.
466 :type pullrequestid: int
467 :type pullrequestid: int
467 :param commit_id: Specify the commit_id for which to set a comment. If
468 :param commit_id: Specify the commit_id for which to set a comment. If
468 given commit_id is different than latest in the PR status
469 given commit_id is different than latest in the PR status
469 change won't be performed.
470 change won't be performed.
470 :type commit_id: str
471 :type commit_id: str
471 :param message: The text content of the comment.
472 :param message: The text content of the comment.
472 :type message: str
473 :type message: str
473 :param status: (**Optional**) Set the approval status of the pull
474 :param status: (**Optional**) Set the approval status of the pull
474 request. One of: 'not_reviewed', 'approved', 'rejected',
475 request. One of: 'not_reviewed', 'approved', 'rejected',
475 'under_review'
476 'under_review'
476 :type status: str
477 :type status: str
477 :param comment_type: Comment type, one of: 'note', 'todo'
478 :param comment_type: Comment type, one of: 'note', 'todo'
478 :type comment_type: Optional(str), default: 'note'
479 :type comment_type: Optional(str), default: 'note'
479 :param resolves_comment_id: id of comment which this one will resolve
480 :param resolves_comment_id: id of comment which this one will resolve
480 :type resolves_comment_id: Optional(int)
481 :type resolves_comment_id: Optional(int)
481 :param extra_recipients: list of user ids or usernames to add
482 :param extra_recipients: list of user ids or usernames to add
482 notifications for this comment. Acts like a CC for notification
483 notifications for this comment. Acts like a CC for notification
483 :type extra_recipients: Optional(list)
484 :type extra_recipients: Optional(list)
484 :param userid: Comment on the pull request as this user
485 :param userid: Comment on the pull request as this user
485 :type userid: Optional(str or int)
486 :type userid: Optional(str or int)
487 :param send_email: Define if this comment should also send email notification
488 :type send_email: Optional(bool)
486
489
487 Example output:
490 Example output:
488
491
489 .. code-block:: bash
492 .. code-block:: bash
490
493
491 id : <id_given_in_input>
494 id : <id_given_in_input>
492 result : {
495 result : {
493 "pull_request_id": "<Integer>",
496 "pull_request_id": "<Integer>",
494 "comment_id": "<Integer>",
497 "comment_id": "<Integer>",
495 "status": {"given": <given_status>,
498 "status": {"given": <given_status>,
496 "was_changed": <bool status_was_actually_changed> },
499 "was_changed": <bool status_was_actually_changed> },
497 },
500 },
498 error : null
501 error : null
499 """
502 """
500 pull_request = get_pull_request_or_error(pullrequestid)
503 pull_request = get_pull_request_or_error(pullrequestid)
501 if Optional.extract(repoid):
504 if Optional.extract(repoid):
502 repo = get_repo_or_error(repoid)
505 repo = get_repo_or_error(repoid)
503 else:
506 else:
504 repo = pull_request.target_repo
507 repo = pull_request.target_repo
505
508
506 auth_user = apiuser
509 auth_user = apiuser
507 if not isinstance(userid, Optional):
510 if not isinstance(userid, Optional):
508 if (has_superadmin_permission(apiuser) or
511 if (has_superadmin_permission(apiuser) or
509 HasRepoPermissionAnyApi('repository.admin')(
512 HasRepoPermissionAnyApi('repository.admin')(
510 user=apiuser, repo_name=repo.repo_name)):
513 user=apiuser, repo_name=repo.repo_name)):
511 apiuser = get_user_or_error(userid)
514 apiuser = get_user_or_error(userid)
512 auth_user = apiuser.AuthUser()
515 auth_user = apiuser.AuthUser()
513 else:
516 else:
514 raise JSONRPCError('userid is not the same as your user')
517 raise JSONRPCError('userid is not the same as your user')
515
518
516 if pull_request.is_closed():
519 if pull_request.is_closed():
517 raise JSONRPCError(
520 raise JSONRPCError(
518 'pull request `%s` comment failed, pull request is closed' % (
521 'pull request `%s` comment failed, pull request is closed' % (
519 pullrequestid,))
522 pullrequestid,))
520
523
521 if not PullRequestModel().check_user_read(
524 if not PullRequestModel().check_user_read(
522 pull_request, apiuser, api=True):
525 pull_request, apiuser, api=True):
523 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
526 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
524 message = Optional.extract(message)
527 message = Optional.extract(message)
525 status = Optional.extract(status)
528 status = Optional.extract(status)
526 commit_id = Optional.extract(commit_id)
529 commit_id = Optional.extract(commit_id)
527 comment_type = Optional.extract(comment_type)
530 comment_type = Optional.extract(comment_type)
528 resolves_comment_id = Optional.extract(resolves_comment_id)
531 resolves_comment_id = Optional.extract(resolves_comment_id)
529 extra_recipients = Optional.extract(extra_recipients)
532 extra_recipients = Optional.extract(extra_recipients)
533 send_email = Optional.extract(send_email, binary=True)
530
534
531 if not message and not status:
535 if not message and not status:
532 raise JSONRPCError(
536 raise JSONRPCError(
533 'Both message and status parameters are missing. '
537 'Both message and status parameters are missing. '
534 'At least one is required.')
538 'At least one is required.')
535
539
536 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
540 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
537 status is not None):
541 status is not None):
538 raise JSONRPCError('Unknown comment status: `%s`' % status)
542 raise JSONRPCError('Unknown comment status: `%s`' % status)
539
543
540 if commit_id and commit_id not in pull_request.revisions:
544 if commit_id and commit_id not in pull_request.revisions:
541 raise JSONRPCError(
545 raise JSONRPCError(
542 'Invalid commit_id `%s` for this pull request.' % commit_id)
546 'Invalid commit_id `%s` for this pull request.' % commit_id)
543
547
544 allowed_to_change_status = PullRequestModel().check_user_change_status(
548 allowed_to_change_status = PullRequestModel().check_user_change_status(
545 pull_request, apiuser)
549 pull_request, apiuser)
546
550
547 # if commit_id is passed re-validated if user is allowed to change status
551 # if commit_id is passed re-validated if user is allowed to change status
548 # based on latest commit_id from the PR
552 # based on latest commit_id from the PR
549 if commit_id:
553 if commit_id:
550 commit_idx = pull_request.revisions.index(commit_id)
554 commit_idx = pull_request.revisions.index(commit_id)
551 if commit_idx != 0:
555 if commit_idx != 0:
552 allowed_to_change_status = False
556 allowed_to_change_status = False
553
557
554 if resolves_comment_id:
558 if resolves_comment_id:
555 comment = ChangesetComment.get(resolves_comment_id)
559 comment = ChangesetComment.get(resolves_comment_id)
556 if not comment:
560 if not comment:
557 raise JSONRPCError(
561 raise JSONRPCError(
558 'Invalid resolves_comment_id `%s` for this pull request.'
562 'Invalid resolves_comment_id `%s` for this pull request.'
559 % resolves_comment_id)
563 % resolves_comment_id)
560 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
564 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
561 raise JSONRPCError(
565 raise JSONRPCError(
562 'Comment `%s` is wrong type for setting status to resolved.'
566 'Comment `%s` is wrong type for setting status to resolved.'
563 % resolves_comment_id)
567 % resolves_comment_id)
564
568
565 text = message
569 text = message
566 status_label = ChangesetStatus.get_status_lbl(status)
570 status_label = ChangesetStatus.get_status_lbl(status)
567 if status and allowed_to_change_status:
571 if status and allowed_to_change_status:
568 st_message = ('Status change %(transition_icon)s %(status)s'
572 st_message = ('Status change %(transition_icon)s %(status)s'
569 % {'transition_icon': '>', 'status': status_label})
573 % {'transition_icon': '>', 'status': status_label})
570 text = message or st_message
574 text = message or st_message
571
575
572 rc_config = SettingsModel().get_all_settings()
576 rc_config = SettingsModel().get_all_settings()
573 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
577 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
574
578
575 status_change = status and allowed_to_change_status
579 status_change = status and allowed_to_change_status
576 comment = CommentsModel().create(
580 comment = CommentsModel().create(
577 text=text,
581 text=text,
578 repo=pull_request.target_repo.repo_id,
582 repo=pull_request.target_repo.repo_id,
579 user=apiuser.user_id,
583 user=apiuser.user_id,
580 pull_request=pull_request.pull_request_id,
584 pull_request=pull_request.pull_request_id,
581 f_path=None,
585 f_path=None,
582 line_no=None,
586 line_no=None,
583 status_change=(status_label if status_change else None),
587 status_change=(status_label if status_change else None),
584 status_change_type=(status if status_change else None),
588 status_change_type=(status if status_change else None),
585 closing_pr=False,
589 closing_pr=False,
586 renderer=renderer,
590 renderer=renderer,
587 comment_type=comment_type,
591 comment_type=comment_type,
588 resolves_comment_id=resolves_comment_id,
592 resolves_comment_id=resolves_comment_id,
589 auth_user=auth_user,
593 auth_user=auth_user,
590 extra_recipients=extra_recipients
594 extra_recipients=extra_recipients,
595 send_email=send_email
591 )
596 )
592
597
593 if allowed_to_change_status and status:
598 if allowed_to_change_status and status:
594 old_calculated_status = pull_request.calculated_review_status()
599 old_calculated_status = pull_request.calculated_review_status()
595 ChangesetStatusModel().set_status(
600 ChangesetStatusModel().set_status(
596 pull_request.target_repo.repo_id,
601 pull_request.target_repo.repo_id,
597 status,
602 status,
598 apiuser.user_id,
603 apiuser.user_id,
599 comment,
604 comment,
600 pull_request=pull_request.pull_request_id
605 pull_request=pull_request.pull_request_id
601 )
606 )
602 Session().flush()
607 Session().flush()
603
608
604 Session().commit()
609 Session().commit()
605
610
606 PullRequestModel().trigger_pull_request_hook(
611 PullRequestModel().trigger_pull_request_hook(
607 pull_request, apiuser, 'comment',
612 pull_request, apiuser, 'comment',
608 data={'comment': comment})
613 data={'comment': comment})
609
614
610 if allowed_to_change_status and status:
615 if allowed_to_change_status and status:
611 # we now calculate the status of pull request, and based on that
616 # we now calculate the status of pull request, and based on that
612 # calculation we set the commits status
617 # calculation we set the commits status
613 calculated_status = pull_request.calculated_review_status()
618 calculated_status = pull_request.calculated_review_status()
614 if old_calculated_status != calculated_status:
619 if old_calculated_status != calculated_status:
615 PullRequestModel().trigger_pull_request_hook(
620 PullRequestModel().trigger_pull_request_hook(
616 pull_request, apiuser, 'review_status_change',
621 pull_request, apiuser, 'review_status_change',
617 data={'status': calculated_status})
622 data={'status': calculated_status})
618
623
619 data = {
624 data = {
620 'pull_request_id': pull_request.pull_request_id,
625 'pull_request_id': pull_request.pull_request_id,
621 'comment_id': comment.comment_id if comment else None,
626 'comment_id': comment.comment_id if comment else None,
622 'status': {'given': status, 'was_changed': status_change},
627 'status': {'given': status, 'was_changed': status_change},
623 }
628 }
624 return data
629 return data
625
630
626
631
627 @jsonrpc_method()
632 @jsonrpc_method()
628 def create_pull_request(
633 def create_pull_request(
629 request, apiuser, source_repo, target_repo, source_ref, target_ref,
634 request, apiuser, source_repo, target_repo, source_ref, target_ref,
630 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
635 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
631 description_renderer=Optional(''), reviewers=Optional(None)):
636 description_renderer=Optional(''), reviewers=Optional(None)):
632 """
637 """
633 Creates a new pull request.
638 Creates a new pull request.
634
639
635 Accepts refs in the following formats:
640 Accepts refs in the following formats:
636
641
637 * branch:<branch_name>:<sha>
642 * branch:<branch_name>:<sha>
638 * branch:<branch_name>
643 * branch:<branch_name>
639 * bookmark:<bookmark_name>:<sha> (Mercurial only)
644 * bookmark:<bookmark_name>:<sha> (Mercurial only)
640 * bookmark:<bookmark_name> (Mercurial only)
645 * bookmark:<bookmark_name> (Mercurial only)
641
646
642 :param apiuser: This is filled automatically from the |authtoken|.
647 :param apiuser: This is filled automatically from the |authtoken|.
643 :type apiuser: AuthUser
648 :type apiuser: AuthUser
644 :param source_repo: Set the source repository name.
649 :param source_repo: Set the source repository name.
645 :type source_repo: str
650 :type source_repo: str
646 :param target_repo: Set the target repository name.
651 :param target_repo: Set the target repository name.
647 :type target_repo: str
652 :type target_repo: str
648 :param source_ref: Set the source ref name.
653 :param source_ref: Set the source ref name.
649 :type source_ref: str
654 :type source_ref: str
650 :param target_ref: Set the target ref name.
655 :param target_ref: Set the target ref name.
651 :type target_ref: str
656 :type target_ref: str
652 :param owner: user_id or username
657 :param owner: user_id or username
653 :type owner: Optional(str)
658 :type owner: Optional(str)
654 :param title: Optionally Set the pull request title, it's generated otherwise
659 :param title: Optionally Set the pull request title, it's generated otherwise
655 :type title: str
660 :type title: str
656 :param description: Set the pull request description.
661 :param description: Set the pull request description.
657 :type description: Optional(str)
662 :type description: Optional(str)
658 :type description_renderer: Optional(str)
663 :type description_renderer: Optional(str)
659 :param description_renderer: Set pull request renderer for the description.
664 :param description_renderer: Set pull request renderer for the description.
660 It should be 'rst', 'markdown' or 'plain'. If not give default
665 It should be 'rst', 'markdown' or 'plain'. If not give default
661 system renderer will be used
666 system renderer will be used
662 :param reviewers: Set the new pull request reviewers list.
667 :param reviewers: Set the new pull request reviewers list.
663 Reviewer defined by review rules will be added automatically to the
668 Reviewer defined by review rules will be added automatically to the
664 defined list.
669 defined list.
665 :type reviewers: Optional(list)
670 :type reviewers: Optional(list)
666 Accepts username strings or objects of the format:
671 Accepts username strings or objects of the format:
667
672
668 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
673 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
669 """
674 """
670
675
671 source_db_repo = get_repo_or_error(source_repo)
676 source_db_repo = get_repo_or_error(source_repo)
672 target_db_repo = get_repo_or_error(target_repo)
677 target_db_repo = get_repo_or_error(target_repo)
673 if not has_superadmin_permission(apiuser):
678 if not has_superadmin_permission(apiuser):
674 _perms = ('repository.admin', 'repository.write', 'repository.read',)
679 _perms = ('repository.admin', 'repository.write', 'repository.read',)
675 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
680 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
676
681
677 owner = validate_set_owner_permissions(apiuser, owner)
682 owner = validate_set_owner_permissions(apiuser, owner)
678
683
679 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
684 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
680 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
685 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
681
686
682 source_scm = source_db_repo.scm_instance()
687 source_scm = source_db_repo.scm_instance()
683 target_scm = target_db_repo.scm_instance()
688 target_scm = target_db_repo.scm_instance()
684
689
685 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
690 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
686 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
691 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
687
692
688 ancestor = source_scm.get_common_ancestor(
693 ancestor = source_scm.get_common_ancestor(
689 source_commit.raw_id, target_commit.raw_id, target_scm)
694 source_commit.raw_id, target_commit.raw_id, target_scm)
690 if not ancestor:
695 if not ancestor:
691 raise JSONRPCError('no common ancestor found')
696 raise JSONRPCError('no common ancestor found')
692
697
693 # recalculate target ref based on ancestor
698 # recalculate target ref based on ancestor
694 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
699 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
695 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
700 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
696
701
697 commit_ranges = target_scm.compare(
702 commit_ranges = target_scm.compare(
698 target_commit.raw_id, source_commit.raw_id, source_scm,
703 target_commit.raw_id, source_commit.raw_id, source_scm,
699 merge=True, pre_load=[])
704 merge=True, pre_load=[])
700
705
701 if not commit_ranges:
706 if not commit_ranges:
702 raise JSONRPCError('no commits found')
707 raise JSONRPCError('no commits found')
703
708
704 reviewer_objects = Optional.extract(reviewers) or []
709 reviewer_objects = Optional.extract(reviewers) or []
705
710
706 # serialize and validate passed in given reviewers
711 # serialize and validate passed in given reviewers
707 if reviewer_objects:
712 if reviewer_objects:
708 schema = ReviewerListSchema()
713 schema = ReviewerListSchema()
709 try:
714 try:
710 reviewer_objects = schema.deserialize(reviewer_objects)
715 reviewer_objects = schema.deserialize(reviewer_objects)
711 except Invalid as err:
716 except Invalid as err:
712 raise JSONRPCValidationError(colander_exc=err)
717 raise JSONRPCValidationError(colander_exc=err)
713
718
714 # validate users
719 # validate users
715 for reviewer_object in reviewer_objects:
720 for reviewer_object in reviewer_objects:
716 user = get_user_or_error(reviewer_object['username'])
721 user = get_user_or_error(reviewer_object['username'])
717 reviewer_object['user_id'] = user.user_id
722 reviewer_object['user_id'] = user.user_id
718
723
719 get_default_reviewers_data, validate_default_reviewers = \
724 get_default_reviewers_data, validate_default_reviewers = \
720 PullRequestModel().get_reviewer_functions()
725 PullRequestModel().get_reviewer_functions()
721
726
722 # recalculate reviewers logic, to make sure we can validate this
727 # recalculate reviewers logic, to make sure we can validate this
723 reviewer_rules = get_default_reviewers_data(
728 reviewer_rules = get_default_reviewers_data(
724 owner, source_db_repo,
729 owner, source_db_repo,
725 source_commit, target_db_repo, target_commit)
730 source_commit, target_db_repo, target_commit)
726
731
727 # now MERGE our given with the calculated
732 # now MERGE our given with the calculated
728 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
733 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
729
734
730 try:
735 try:
731 reviewers = validate_default_reviewers(
736 reviewers = validate_default_reviewers(
732 reviewer_objects, reviewer_rules)
737 reviewer_objects, reviewer_rules)
733 except ValueError as e:
738 except ValueError as e:
734 raise JSONRPCError('Reviewers Validation: {}'.format(e))
739 raise JSONRPCError('Reviewers Validation: {}'.format(e))
735
740
736 title = Optional.extract(title)
741 title = Optional.extract(title)
737 if not title:
742 if not title:
738 title_source_ref = source_ref.split(':', 2)[1]
743 title_source_ref = source_ref.split(':', 2)[1]
739 title = PullRequestModel().generate_pullrequest_title(
744 title = PullRequestModel().generate_pullrequest_title(
740 source=source_repo,
745 source=source_repo,
741 source_ref=title_source_ref,
746 source_ref=title_source_ref,
742 target=target_repo
747 target=target_repo
743 )
748 )
744 # fetch renderer, if set fallback to plain in case of PR
749 # fetch renderer, if set fallback to plain in case of PR
745 rc_config = SettingsModel().get_all_settings()
750 rc_config = SettingsModel().get_all_settings()
746 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
751 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
747 description = Optional.extract(description)
752 description = Optional.extract(description)
748 description_renderer = Optional.extract(description_renderer) or default_system_renderer
753 description_renderer = Optional.extract(description_renderer) or default_system_renderer
749
754
750 pull_request = PullRequestModel().create(
755 pull_request = PullRequestModel().create(
751 created_by=owner.user_id,
756 created_by=owner.user_id,
752 source_repo=source_repo,
757 source_repo=source_repo,
753 source_ref=full_source_ref,
758 source_ref=full_source_ref,
754 target_repo=target_repo,
759 target_repo=target_repo,
755 target_ref=full_target_ref,
760 target_ref=full_target_ref,
756 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
761 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
757 reviewers=reviewers,
762 reviewers=reviewers,
758 title=title,
763 title=title,
759 description=description,
764 description=description,
760 description_renderer=description_renderer,
765 description_renderer=description_renderer,
761 reviewer_data=reviewer_rules,
766 reviewer_data=reviewer_rules,
762 auth_user=apiuser
767 auth_user=apiuser
763 )
768 )
764
769
765 Session().commit()
770 Session().commit()
766 data = {
771 data = {
767 'msg': 'Created new pull request `{}`'.format(title),
772 'msg': 'Created new pull request `{}`'.format(title),
768 'pull_request_id': pull_request.pull_request_id,
773 'pull_request_id': pull_request.pull_request_id,
769 }
774 }
770 return data
775 return data
771
776
772
777
773 @jsonrpc_method()
778 @jsonrpc_method()
774 def update_pull_request(
779 def update_pull_request(
775 request, apiuser, pullrequestid, repoid=Optional(None),
780 request, apiuser, pullrequestid, repoid=Optional(None),
776 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
781 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
777 reviewers=Optional(None), update_commits=Optional(None)):
782 reviewers=Optional(None), update_commits=Optional(None)):
778 """
783 """
779 Updates a pull request.
784 Updates a pull request.
780
785
781 :param apiuser: This is filled automatically from the |authtoken|.
786 :param apiuser: This is filled automatically from the |authtoken|.
782 :type apiuser: AuthUser
787 :type apiuser: AuthUser
783 :param repoid: Optional repository name or repository ID.
788 :param repoid: Optional repository name or repository ID.
784 :type repoid: str or int
789 :type repoid: str or int
785 :param pullrequestid: The pull request ID.
790 :param pullrequestid: The pull request ID.
786 :type pullrequestid: int
791 :type pullrequestid: int
787 :param title: Set the pull request title.
792 :param title: Set the pull request title.
788 :type title: str
793 :type title: str
789 :param description: Update pull request description.
794 :param description: Update pull request description.
790 :type description: Optional(str)
795 :type description: Optional(str)
791 :type description_renderer: Optional(str)
796 :type description_renderer: Optional(str)
792 :param description_renderer: Update pull request renderer for the description.
797 :param description_renderer: Update pull request renderer for the description.
793 It should be 'rst', 'markdown' or 'plain'
798 It should be 'rst', 'markdown' or 'plain'
794 :param reviewers: Update pull request reviewers list with new value.
799 :param reviewers: Update pull request reviewers list with new value.
795 :type reviewers: Optional(list)
800 :type reviewers: Optional(list)
796 Accepts username strings or objects of the format:
801 Accepts username strings or objects of the format:
797
802
798 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
803 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
799
804
800 :param update_commits: Trigger update of commits for this pull request
805 :param update_commits: Trigger update of commits for this pull request
801 :type: update_commits: Optional(bool)
806 :type: update_commits: Optional(bool)
802
807
803 Example output:
808 Example output:
804
809
805 .. code-block:: bash
810 .. code-block:: bash
806
811
807 id : <id_given_in_input>
812 id : <id_given_in_input>
808 result : {
813 result : {
809 "msg": "Updated pull request `63`",
814 "msg": "Updated pull request `63`",
810 "pull_request": <pull_request_object>,
815 "pull_request": <pull_request_object>,
811 "updated_reviewers": {
816 "updated_reviewers": {
812 "added": [
817 "added": [
813 "username"
818 "username"
814 ],
819 ],
815 "removed": []
820 "removed": []
816 },
821 },
817 "updated_commits": {
822 "updated_commits": {
818 "added": [
823 "added": [
819 "<sha1_hash>"
824 "<sha1_hash>"
820 ],
825 ],
821 "common": [
826 "common": [
822 "<sha1_hash>",
827 "<sha1_hash>",
823 "<sha1_hash>",
828 "<sha1_hash>",
824 ],
829 ],
825 "removed": []
830 "removed": []
826 }
831 }
827 }
832 }
828 error : null
833 error : null
829 """
834 """
830
835
831 pull_request = get_pull_request_or_error(pullrequestid)
836 pull_request = get_pull_request_or_error(pullrequestid)
832 if Optional.extract(repoid):
837 if Optional.extract(repoid):
833 repo = get_repo_or_error(repoid)
838 repo = get_repo_or_error(repoid)
834 else:
839 else:
835 repo = pull_request.target_repo
840 repo = pull_request.target_repo
836
841
837 if not PullRequestModel().check_user_update(
842 if not PullRequestModel().check_user_update(
838 pull_request, apiuser, api=True):
843 pull_request, apiuser, api=True):
839 raise JSONRPCError(
844 raise JSONRPCError(
840 'pull request `%s` update failed, no permission to update.' % (
845 'pull request `%s` update failed, no permission to update.' % (
841 pullrequestid,))
846 pullrequestid,))
842 if pull_request.is_closed():
847 if pull_request.is_closed():
843 raise JSONRPCError(
848 raise JSONRPCError(
844 'pull request `%s` update failed, pull request is closed' % (
849 'pull request `%s` update failed, pull request is closed' % (
845 pullrequestid,))
850 pullrequestid,))
846
851
847 reviewer_objects = Optional.extract(reviewers) or []
852 reviewer_objects = Optional.extract(reviewers) or []
848
853
849 if reviewer_objects:
854 if reviewer_objects:
850 schema = ReviewerListSchema()
855 schema = ReviewerListSchema()
851 try:
856 try:
852 reviewer_objects = schema.deserialize(reviewer_objects)
857 reviewer_objects = schema.deserialize(reviewer_objects)
853 except Invalid as err:
858 except Invalid as err:
854 raise JSONRPCValidationError(colander_exc=err)
859 raise JSONRPCValidationError(colander_exc=err)
855
860
856 # validate users
861 # validate users
857 for reviewer_object in reviewer_objects:
862 for reviewer_object in reviewer_objects:
858 user = get_user_or_error(reviewer_object['username'])
863 user = get_user_or_error(reviewer_object['username'])
859 reviewer_object['user_id'] = user.user_id
864 reviewer_object['user_id'] = user.user_id
860
865
861 get_default_reviewers_data, get_validated_reviewers = \
866 get_default_reviewers_data, get_validated_reviewers = \
862 PullRequestModel().get_reviewer_functions()
867 PullRequestModel().get_reviewer_functions()
863
868
864 # re-use stored rules
869 # re-use stored rules
865 reviewer_rules = pull_request.reviewer_data
870 reviewer_rules = pull_request.reviewer_data
866 try:
871 try:
867 reviewers = get_validated_reviewers(
872 reviewers = get_validated_reviewers(
868 reviewer_objects, reviewer_rules)
873 reviewer_objects, reviewer_rules)
869 except ValueError as e:
874 except ValueError as e:
870 raise JSONRPCError('Reviewers Validation: {}'.format(e))
875 raise JSONRPCError('Reviewers Validation: {}'.format(e))
871 else:
876 else:
872 reviewers = []
877 reviewers = []
873
878
874 title = Optional.extract(title)
879 title = Optional.extract(title)
875 description = Optional.extract(description)
880 description = Optional.extract(description)
876 description_renderer = Optional.extract(description_renderer)
881 description_renderer = Optional.extract(description_renderer)
877
882
878 if title or description:
883 if title or description:
879 PullRequestModel().edit(
884 PullRequestModel().edit(
880 pull_request,
885 pull_request,
881 title or pull_request.title,
886 title or pull_request.title,
882 description or pull_request.description,
887 description or pull_request.description,
883 description_renderer or pull_request.description_renderer,
888 description_renderer or pull_request.description_renderer,
884 apiuser)
889 apiuser)
885 Session().commit()
890 Session().commit()
886
891
887 commit_changes = {"added": [], "common": [], "removed": []}
892 commit_changes = {"added": [], "common": [], "removed": []}
888 if str2bool(Optional.extract(update_commits)):
893 if str2bool(Optional.extract(update_commits)):
889
894
890 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
895 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
891 raise JSONRPCError(
896 raise JSONRPCError(
892 'Operation forbidden because pull request is in state {}, '
897 'Operation forbidden because pull request is in state {}, '
893 'only state {} is allowed.'.format(
898 'only state {} is allowed.'.format(
894 pull_request.pull_request_state, PullRequest.STATE_CREATED))
899 pull_request.pull_request_state, PullRequest.STATE_CREATED))
895
900
896 with pull_request.set_state(PullRequest.STATE_UPDATING):
901 with pull_request.set_state(PullRequest.STATE_UPDATING):
897 if PullRequestModel().has_valid_update_type(pull_request):
902 if PullRequestModel().has_valid_update_type(pull_request):
898 db_user = apiuser.get_instance()
903 db_user = apiuser.get_instance()
899 update_response = PullRequestModel().update_commits(
904 update_response = PullRequestModel().update_commits(
900 pull_request, db_user)
905 pull_request, db_user)
901 commit_changes = update_response.changes or commit_changes
906 commit_changes = update_response.changes or commit_changes
902 Session().commit()
907 Session().commit()
903
908
904 reviewers_changes = {"added": [], "removed": []}
909 reviewers_changes = {"added": [], "removed": []}
905 if reviewers:
910 if reviewers:
906 old_calculated_status = pull_request.calculated_review_status()
911 old_calculated_status = pull_request.calculated_review_status()
907 added_reviewers, removed_reviewers = \
912 added_reviewers, removed_reviewers = \
908 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
913 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
909
914
910 reviewers_changes['added'] = sorted(
915 reviewers_changes['added'] = sorted(
911 [get_user_or_error(n).username for n in added_reviewers])
916 [get_user_or_error(n).username for n in added_reviewers])
912 reviewers_changes['removed'] = sorted(
917 reviewers_changes['removed'] = sorted(
913 [get_user_or_error(n).username for n in removed_reviewers])
918 [get_user_or_error(n).username for n in removed_reviewers])
914 Session().commit()
919 Session().commit()
915
920
916 # trigger status changed if change in reviewers changes the status
921 # trigger status changed if change in reviewers changes the status
917 calculated_status = pull_request.calculated_review_status()
922 calculated_status = pull_request.calculated_review_status()
918 if old_calculated_status != calculated_status:
923 if old_calculated_status != calculated_status:
919 PullRequestModel().trigger_pull_request_hook(
924 PullRequestModel().trigger_pull_request_hook(
920 pull_request, apiuser, 'review_status_change',
925 pull_request, apiuser, 'review_status_change',
921 data={'status': calculated_status})
926 data={'status': calculated_status})
922
927
923 data = {
928 data = {
924 'msg': 'Updated pull request `{}`'.format(
929 'msg': 'Updated pull request `{}`'.format(
925 pull_request.pull_request_id),
930 pull_request.pull_request_id),
926 'pull_request': pull_request.get_api_data(),
931 'pull_request': pull_request.get_api_data(),
927 'updated_commits': commit_changes,
932 'updated_commits': commit_changes,
928 'updated_reviewers': reviewers_changes
933 'updated_reviewers': reviewers_changes
929 }
934 }
930
935
931 return data
936 return data
932
937
933
938
934 @jsonrpc_method()
939 @jsonrpc_method()
935 def close_pull_request(
940 def close_pull_request(
936 request, apiuser, pullrequestid, repoid=Optional(None),
941 request, apiuser, pullrequestid, repoid=Optional(None),
937 userid=Optional(OAttr('apiuser')), message=Optional('')):
942 userid=Optional(OAttr('apiuser')), message=Optional('')):
938 """
943 """
939 Close the pull request specified by `pullrequestid`.
944 Close the pull request specified by `pullrequestid`.
940
945
941 :param apiuser: This is filled automatically from the |authtoken|.
946 :param apiuser: This is filled automatically from the |authtoken|.
942 :type apiuser: AuthUser
947 :type apiuser: AuthUser
943 :param repoid: Repository name or repository ID to which the pull
948 :param repoid: Repository name or repository ID to which the pull
944 request belongs.
949 request belongs.
945 :type repoid: str or int
950 :type repoid: str or int
946 :param pullrequestid: ID of the pull request to be closed.
951 :param pullrequestid: ID of the pull request to be closed.
947 :type pullrequestid: int
952 :type pullrequestid: int
948 :param userid: Close the pull request as this user.
953 :param userid: Close the pull request as this user.
949 :type userid: Optional(str or int)
954 :type userid: Optional(str or int)
950 :param message: Optional message to close the Pull Request with. If not
955 :param message: Optional message to close the Pull Request with. If not
951 specified it will be generated automatically.
956 specified it will be generated automatically.
952 :type message: Optional(str)
957 :type message: Optional(str)
953
958
954 Example output:
959 Example output:
955
960
956 .. code-block:: bash
961 .. code-block:: bash
957
962
958 "id": <id_given_in_input>,
963 "id": <id_given_in_input>,
959 "result": {
964 "result": {
960 "pull_request_id": "<int>",
965 "pull_request_id": "<int>",
961 "close_status": "<str:status_lbl>,
966 "close_status": "<str:status_lbl>,
962 "closed": "<bool>"
967 "closed": "<bool>"
963 },
968 },
964 "error": null
969 "error": null
965
970
966 """
971 """
967 _ = request.translate
972 _ = request.translate
968
973
969 pull_request = get_pull_request_or_error(pullrequestid)
974 pull_request = get_pull_request_or_error(pullrequestid)
970 if Optional.extract(repoid):
975 if Optional.extract(repoid):
971 repo = get_repo_or_error(repoid)
976 repo = get_repo_or_error(repoid)
972 else:
977 else:
973 repo = pull_request.target_repo
978 repo = pull_request.target_repo
974
979
975 if not isinstance(userid, Optional):
980 if not isinstance(userid, Optional):
976 if (has_superadmin_permission(apiuser) or
981 if (has_superadmin_permission(apiuser) or
977 HasRepoPermissionAnyApi('repository.admin')(
982 HasRepoPermissionAnyApi('repository.admin')(
978 user=apiuser, repo_name=repo.repo_name)):
983 user=apiuser, repo_name=repo.repo_name)):
979 apiuser = get_user_or_error(userid)
984 apiuser = get_user_or_error(userid)
980 else:
985 else:
981 raise JSONRPCError('userid is not the same as your user')
986 raise JSONRPCError('userid is not the same as your user')
982
987
983 if pull_request.is_closed():
988 if pull_request.is_closed():
984 raise JSONRPCError(
989 raise JSONRPCError(
985 'pull request `%s` is already closed' % (pullrequestid,))
990 'pull request `%s` is already closed' % (pullrequestid,))
986
991
987 # only owner or admin or person with write permissions
992 # only owner or admin or person with write permissions
988 allowed_to_close = PullRequestModel().check_user_update(
993 allowed_to_close = PullRequestModel().check_user_update(
989 pull_request, apiuser, api=True)
994 pull_request, apiuser, api=True)
990
995
991 if not allowed_to_close:
996 if not allowed_to_close:
992 raise JSONRPCError(
997 raise JSONRPCError(
993 'pull request `%s` close failed, no permission to close.' % (
998 'pull request `%s` close failed, no permission to close.' % (
994 pullrequestid,))
999 pullrequestid,))
995
1000
996 # message we're using to close the PR, else it's automatically generated
1001 # message we're using to close the PR, else it's automatically generated
997 message = Optional.extract(message)
1002 message = Optional.extract(message)
998
1003
999 # finally close the PR, with proper message comment
1004 # finally close the PR, with proper message comment
1000 comment, status = PullRequestModel().close_pull_request_with_comment(
1005 comment, status = PullRequestModel().close_pull_request_with_comment(
1001 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1006 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1002 status_lbl = ChangesetStatus.get_status_lbl(status)
1007 status_lbl = ChangesetStatus.get_status_lbl(status)
1003
1008
1004 Session().commit()
1009 Session().commit()
1005
1010
1006 data = {
1011 data = {
1007 'pull_request_id': pull_request.pull_request_id,
1012 'pull_request_id': pull_request.pull_request_id,
1008 'close_status': status_lbl,
1013 'close_status': status_lbl,
1009 'closed': True,
1014 'closed': True,
1010 }
1015 }
1011 return data
1016 return data
@@ -1,2339 +1,2343 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache
32 from rhodecode.lib import audit_logger, rc_cache
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 from rhodecode.lib.celerylib.utils import get_task_id
35 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 from rhodecode.lib.vcs import RepositoryError
39 from rhodecode.lib.vcs import RepositoryError
40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 from rhodecode.model.changeset_status import ChangesetStatusModel
41 from rhodecode.model.changeset_status import ChangesetStatusModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
44 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 ChangesetComment)
45 ChangesetComment)
46 from rhodecode.model.permission import PermissionModel
46 from rhodecode.model.permission import PermissionModel
47 from rhodecode.model.repo import RepoModel
47 from rhodecode.model.repo import RepoModel
48 from rhodecode.model.scm import ScmModel, RepoList
48 from rhodecode.model.scm import ScmModel, RepoList
49 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
49 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
50 from rhodecode.model import validation_schema
50 from rhodecode.model import validation_schema
51 from rhodecode.model.validation_schema.schemas import repo_schema
51 from rhodecode.model.validation_schema.schemas import repo_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 @jsonrpc_method()
56 @jsonrpc_method()
57 def get_repo(request, apiuser, repoid, cache=Optional(True)):
57 def get_repo(request, apiuser, repoid, cache=Optional(True)):
58 """
58 """
59 Gets an existing repository by its name or repository_id.
59 Gets an existing repository by its name or repository_id.
60
60
61 The members section so the output returns users groups or users
61 The members section so the output returns users groups or users
62 associated with that repository.
62 associated with that repository.
63
63
64 This command can only be run using an |authtoken| with admin rights,
64 This command can only be run using an |authtoken| with admin rights,
65 or users with at least read rights to the |repo|.
65 or users with at least read rights to the |repo|.
66
66
67 :param apiuser: This is filled automatically from the |authtoken|.
67 :param apiuser: This is filled automatically from the |authtoken|.
68 :type apiuser: AuthUser
68 :type apiuser: AuthUser
69 :param repoid: The repository name or repository id.
69 :param repoid: The repository name or repository id.
70 :type repoid: str or int
70 :type repoid: str or int
71 :param cache: use the cached value for last changeset
71 :param cache: use the cached value for last changeset
72 :type: cache: Optional(bool)
72 :type: cache: Optional(bool)
73
73
74 Example output:
74 Example output:
75
75
76 .. code-block:: bash
76 .. code-block:: bash
77
77
78 {
78 {
79 "error": null,
79 "error": null,
80 "id": <repo_id>,
80 "id": <repo_id>,
81 "result": {
81 "result": {
82 "clone_uri": null,
82 "clone_uri": null,
83 "created_on": "timestamp",
83 "created_on": "timestamp",
84 "description": "repo description",
84 "description": "repo description",
85 "enable_downloads": false,
85 "enable_downloads": false,
86 "enable_locking": false,
86 "enable_locking": false,
87 "enable_statistics": false,
87 "enable_statistics": false,
88 "followers": [
88 "followers": [
89 {
89 {
90 "active": true,
90 "active": true,
91 "admin": false,
91 "admin": false,
92 "api_key": "****************************************",
92 "api_key": "****************************************",
93 "api_keys": [
93 "api_keys": [
94 "****************************************"
94 "****************************************"
95 ],
95 ],
96 "email": "user@example.com",
96 "email": "user@example.com",
97 "emails": [
97 "emails": [
98 "user@example.com"
98 "user@example.com"
99 ],
99 ],
100 "extern_name": "rhodecode",
100 "extern_name": "rhodecode",
101 "extern_type": "rhodecode",
101 "extern_type": "rhodecode",
102 "firstname": "username",
102 "firstname": "username",
103 "ip_addresses": [],
103 "ip_addresses": [],
104 "language": null,
104 "language": null,
105 "last_login": "2015-09-16T17:16:35.854",
105 "last_login": "2015-09-16T17:16:35.854",
106 "lastname": "surname",
106 "lastname": "surname",
107 "user_id": <user_id>,
107 "user_id": <user_id>,
108 "username": "name"
108 "username": "name"
109 }
109 }
110 ],
110 ],
111 "fork_of": "parent-repo",
111 "fork_of": "parent-repo",
112 "landing_rev": [
112 "landing_rev": [
113 "rev",
113 "rev",
114 "tip"
114 "tip"
115 ],
115 ],
116 "last_changeset": {
116 "last_changeset": {
117 "author": "User <user@example.com>",
117 "author": "User <user@example.com>",
118 "branch": "default",
118 "branch": "default",
119 "date": "timestamp",
119 "date": "timestamp",
120 "message": "last commit message",
120 "message": "last commit message",
121 "parents": [
121 "parents": [
122 {
122 {
123 "raw_id": "commit-id"
123 "raw_id": "commit-id"
124 }
124 }
125 ],
125 ],
126 "raw_id": "commit-id",
126 "raw_id": "commit-id",
127 "revision": <revision number>,
127 "revision": <revision number>,
128 "short_id": "short id"
128 "short_id": "short id"
129 },
129 },
130 "lock_reason": null,
130 "lock_reason": null,
131 "locked_by": null,
131 "locked_by": null,
132 "locked_date": null,
132 "locked_date": null,
133 "owner": "owner-name",
133 "owner": "owner-name",
134 "permissions": [
134 "permissions": [
135 {
135 {
136 "name": "super-admin-name",
136 "name": "super-admin-name",
137 "origin": "super-admin",
137 "origin": "super-admin",
138 "permission": "repository.admin",
138 "permission": "repository.admin",
139 "type": "user"
139 "type": "user"
140 },
140 },
141 {
141 {
142 "name": "owner-name",
142 "name": "owner-name",
143 "origin": "owner",
143 "origin": "owner",
144 "permission": "repository.admin",
144 "permission": "repository.admin",
145 "type": "user"
145 "type": "user"
146 },
146 },
147 {
147 {
148 "name": "user-group-name",
148 "name": "user-group-name",
149 "origin": "permission",
149 "origin": "permission",
150 "permission": "repository.write",
150 "permission": "repository.write",
151 "type": "user_group"
151 "type": "user_group"
152 }
152 }
153 ],
153 ],
154 "private": true,
154 "private": true,
155 "repo_id": 676,
155 "repo_id": 676,
156 "repo_name": "user-group/repo-name",
156 "repo_name": "user-group/repo-name",
157 "repo_type": "hg"
157 "repo_type": "hg"
158 }
158 }
159 }
159 }
160 """
160 """
161
161
162 repo = get_repo_or_error(repoid)
162 repo = get_repo_or_error(repoid)
163 cache = Optional.extract(cache)
163 cache = Optional.extract(cache)
164
164
165 include_secrets = False
165 include_secrets = False
166 if has_superadmin_permission(apiuser):
166 if has_superadmin_permission(apiuser):
167 include_secrets = True
167 include_secrets = True
168 else:
168 else:
169 # check if we have at least read permission for this repo !
169 # check if we have at least read permission for this repo !
170 _perms = (
170 _perms = (
171 'repository.admin', 'repository.write', 'repository.read',)
171 'repository.admin', 'repository.write', 'repository.read',)
172 validate_repo_permissions(apiuser, repoid, repo, _perms)
172 validate_repo_permissions(apiuser, repoid, repo, _perms)
173
173
174 permissions = []
174 permissions = []
175 for _user in repo.permissions():
175 for _user in repo.permissions():
176 user_data = {
176 user_data = {
177 'name': _user.username,
177 'name': _user.username,
178 'permission': _user.permission,
178 'permission': _user.permission,
179 'origin': get_origin(_user),
179 'origin': get_origin(_user),
180 'type': "user",
180 'type': "user",
181 }
181 }
182 permissions.append(user_data)
182 permissions.append(user_data)
183
183
184 for _user_group in repo.permission_user_groups():
184 for _user_group in repo.permission_user_groups():
185 user_group_data = {
185 user_group_data = {
186 'name': _user_group.users_group_name,
186 'name': _user_group.users_group_name,
187 'permission': _user_group.permission,
187 'permission': _user_group.permission,
188 'origin': get_origin(_user_group),
188 'origin': get_origin(_user_group),
189 'type': "user_group",
189 'type': "user_group",
190 }
190 }
191 permissions.append(user_group_data)
191 permissions.append(user_group_data)
192
192
193 following_users = [
193 following_users = [
194 user.user.get_api_data(include_secrets=include_secrets)
194 user.user.get_api_data(include_secrets=include_secrets)
195 for user in repo.followers]
195 for user in repo.followers]
196
196
197 if not cache:
197 if not cache:
198 repo.update_commit_cache()
198 repo.update_commit_cache()
199 data = repo.get_api_data(include_secrets=include_secrets)
199 data = repo.get_api_data(include_secrets=include_secrets)
200 data['permissions'] = permissions
200 data['permissions'] = permissions
201 data['followers'] = following_users
201 data['followers'] = following_users
202 return data
202 return data
203
203
204
204
205 @jsonrpc_method()
205 @jsonrpc_method()
206 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
206 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
207 """
207 """
208 Lists all existing repositories.
208 Lists all existing repositories.
209
209
210 This command can only be run using an |authtoken| with admin rights,
210 This command can only be run using an |authtoken| with admin rights,
211 or users with at least read rights to |repos|.
211 or users with at least read rights to |repos|.
212
212
213 :param apiuser: This is filled automatically from the |authtoken|.
213 :param apiuser: This is filled automatically from the |authtoken|.
214 :type apiuser: AuthUser
214 :type apiuser: AuthUser
215 :param root: specify root repository group to fetch repositories.
215 :param root: specify root repository group to fetch repositories.
216 filters the returned repositories to be members of given root group.
216 filters the returned repositories to be members of given root group.
217 :type root: Optional(None)
217 :type root: Optional(None)
218 :param traverse: traverse given root into subrepositories. With this flag
218 :param traverse: traverse given root into subrepositories. With this flag
219 set to False, it will only return top-level repositories from `root`.
219 set to False, it will only return top-level repositories from `root`.
220 if root is empty it will return just top-level repositories.
220 if root is empty it will return just top-level repositories.
221 :type traverse: Optional(True)
221 :type traverse: Optional(True)
222
222
223
223
224 Example output:
224 Example output:
225
225
226 .. code-block:: bash
226 .. code-block:: bash
227
227
228 id : <id_given_in_input>
228 id : <id_given_in_input>
229 result: [
229 result: [
230 {
230 {
231 "repo_id" : "<repo_id>",
231 "repo_id" : "<repo_id>",
232 "repo_name" : "<reponame>"
232 "repo_name" : "<reponame>"
233 "repo_type" : "<repo_type>",
233 "repo_type" : "<repo_type>",
234 "clone_uri" : "<clone_uri>",
234 "clone_uri" : "<clone_uri>",
235 "private": : "<bool>",
235 "private": : "<bool>",
236 "created_on" : "<datetimecreated>",
236 "created_on" : "<datetimecreated>",
237 "description" : "<description>",
237 "description" : "<description>",
238 "landing_rev": "<landing_rev>",
238 "landing_rev": "<landing_rev>",
239 "owner": "<repo_owner>",
239 "owner": "<repo_owner>",
240 "fork_of": "<name_of_fork_parent>",
240 "fork_of": "<name_of_fork_parent>",
241 "enable_downloads": "<bool>",
241 "enable_downloads": "<bool>",
242 "enable_locking": "<bool>",
242 "enable_locking": "<bool>",
243 "enable_statistics": "<bool>",
243 "enable_statistics": "<bool>",
244 },
244 },
245 ...
245 ...
246 ]
246 ]
247 error: null
247 error: null
248 """
248 """
249
249
250 include_secrets = has_superadmin_permission(apiuser)
250 include_secrets = has_superadmin_permission(apiuser)
251 _perms = ('repository.read', 'repository.write', 'repository.admin',)
251 _perms = ('repository.read', 'repository.write', 'repository.admin',)
252 extras = {'user': apiuser}
252 extras = {'user': apiuser}
253
253
254 root = Optional.extract(root)
254 root = Optional.extract(root)
255 traverse = Optional.extract(traverse, binary=True)
255 traverse = Optional.extract(traverse, binary=True)
256
256
257 if root:
257 if root:
258 # verify parent existance, if it's empty return an error
258 # verify parent existance, if it's empty return an error
259 parent = RepoGroup.get_by_group_name(root)
259 parent = RepoGroup.get_by_group_name(root)
260 if not parent:
260 if not parent:
261 raise JSONRPCError(
261 raise JSONRPCError(
262 'Root repository group `{}` does not exist'.format(root))
262 'Root repository group `{}` does not exist'.format(root))
263
263
264 if traverse:
264 if traverse:
265 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
265 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
266 else:
266 else:
267 repos = RepoModel().get_repos_for_root(root=parent)
267 repos = RepoModel().get_repos_for_root(root=parent)
268 else:
268 else:
269 if traverse:
269 if traverse:
270 repos = RepoModel().get_all()
270 repos = RepoModel().get_all()
271 else:
271 else:
272 # return just top-level
272 # return just top-level
273 repos = RepoModel().get_repos_for_root(root=None)
273 repos = RepoModel().get_repos_for_root(root=None)
274
274
275 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
275 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
276 return [repo.get_api_data(include_secrets=include_secrets)
276 return [repo.get_api_data(include_secrets=include_secrets)
277 for repo in repo_list]
277 for repo in repo_list]
278
278
279
279
280 @jsonrpc_method()
280 @jsonrpc_method()
281 def get_repo_changeset(request, apiuser, repoid, revision,
281 def get_repo_changeset(request, apiuser, repoid, revision,
282 details=Optional('basic')):
282 details=Optional('basic')):
283 """
283 """
284 Returns information about a changeset.
284 Returns information about a changeset.
285
285
286 Additionally parameters define the amount of details returned by
286 Additionally parameters define the amount of details returned by
287 this function.
287 this function.
288
288
289 This command can only be run using an |authtoken| with admin rights,
289 This command can only be run using an |authtoken| with admin rights,
290 or users with at least read rights to the |repo|.
290 or users with at least read rights to the |repo|.
291
291
292 :param apiuser: This is filled automatically from the |authtoken|.
292 :param apiuser: This is filled automatically from the |authtoken|.
293 :type apiuser: AuthUser
293 :type apiuser: AuthUser
294 :param repoid: The repository name or repository id
294 :param repoid: The repository name or repository id
295 :type repoid: str or int
295 :type repoid: str or int
296 :param revision: revision for which listing should be done
296 :param revision: revision for which listing should be done
297 :type revision: str
297 :type revision: str
298 :param details: details can be 'basic|extended|full' full gives diff
298 :param details: details can be 'basic|extended|full' full gives diff
299 info details like the diff itself, and number of changed files etc.
299 info details like the diff itself, and number of changed files etc.
300 :type details: Optional(str)
300 :type details: Optional(str)
301
301
302 """
302 """
303 repo = get_repo_or_error(repoid)
303 repo = get_repo_or_error(repoid)
304 if not has_superadmin_permission(apiuser):
304 if not has_superadmin_permission(apiuser):
305 _perms = (
305 _perms = (
306 'repository.admin', 'repository.write', 'repository.read',)
306 'repository.admin', 'repository.write', 'repository.read',)
307 validate_repo_permissions(apiuser, repoid, repo, _perms)
307 validate_repo_permissions(apiuser, repoid, repo, _perms)
308
308
309 changes_details = Optional.extract(details)
309 changes_details = Optional.extract(details)
310 _changes_details_types = ['basic', 'extended', 'full']
310 _changes_details_types = ['basic', 'extended', 'full']
311 if changes_details not in _changes_details_types:
311 if changes_details not in _changes_details_types:
312 raise JSONRPCError(
312 raise JSONRPCError(
313 'ret_type must be one of %s' % (
313 'ret_type must be one of %s' % (
314 ','.join(_changes_details_types)))
314 ','.join(_changes_details_types)))
315
315
316 pre_load = ['author', 'branch', 'date', 'message', 'parents',
316 pre_load = ['author', 'branch', 'date', 'message', 'parents',
317 'status', '_commit', '_file_paths']
317 'status', '_commit', '_file_paths']
318
318
319 try:
319 try:
320 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
320 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
321 except TypeError as e:
321 except TypeError as e:
322 raise JSONRPCError(safe_str(e))
322 raise JSONRPCError(safe_str(e))
323 _cs_json = cs.__json__()
323 _cs_json = cs.__json__()
324 _cs_json['diff'] = build_commit_data(cs, changes_details)
324 _cs_json['diff'] = build_commit_data(cs, changes_details)
325 if changes_details == 'full':
325 if changes_details == 'full':
326 _cs_json['refs'] = cs._get_refs()
326 _cs_json['refs'] = cs._get_refs()
327 return _cs_json
327 return _cs_json
328
328
329
329
330 @jsonrpc_method()
330 @jsonrpc_method()
331 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
331 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
332 details=Optional('basic')):
332 details=Optional('basic')):
333 """
333 """
334 Returns a set of commits limited by the number starting
334 Returns a set of commits limited by the number starting
335 from the `start_rev` option.
335 from the `start_rev` option.
336
336
337 Additional parameters define the amount of details returned by this
337 Additional parameters define the amount of details returned by this
338 function.
338 function.
339
339
340 This command can only be run using an |authtoken| with admin rights,
340 This command can only be run using an |authtoken| with admin rights,
341 or users with at least read rights to |repos|.
341 or users with at least read rights to |repos|.
342
342
343 :param apiuser: This is filled automatically from the |authtoken|.
343 :param apiuser: This is filled automatically from the |authtoken|.
344 :type apiuser: AuthUser
344 :type apiuser: AuthUser
345 :param repoid: The repository name or repository ID.
345 :param repoid: The repository name or repository ID.
346 :type repoid: str or int
346 :type repoid: str or int
347 :param start_rev: The starting revision from where to get changesets.
347 :param start_rev: The starting revision from where to get changesets.
348 :type start_rev: str
348 :type start_rev: str
349 :param limit: Limit the number of commits to this amount
349 :param limit: Limit the number of commits to this amount
350 :type limit: str or int
350 :type limit: str or int
351 :param details: Set the level of detail returned. Valid option are:
351 :param details: Set the level of detail returned. Valid option are:
352 ``basic``, ``extended`` and ``full``.
352 ``basic``, ``extended`` and ``full``.
353 :type details: Optional(str)
353 :type details: Optional(str)
354
354
355 .. note::
355 .. note::
356
356
357 Setting the parameter `details` to the value ``full`` is extensive
357 Setting the parameter `details` to the value ``full`` is extensive
358 and returns details like the diff itself, and the number
358 and returns details like the diff itself, and the number
359 of changed files.
359 of changed files.
360
360
361 """
361 """
362 repo = get_repo_or_error(repoid)
362 repo = get_repo_or_error(repoid)
363 if not has_superadmin_permission(apiuser):
363 if not has_superadmin_permission(apiuser):
364 _perms = (
364 _perms = (
365 'repository.admin', 'repository.write', 'repository.read',)
365 'repository.admin', 'repository.write', 'repository.read',)
366 validate_repo_permissions(apiuser, repoid, repo, _perms)
366 validate_repo_permissions(apiuser, repoid, repo, _perms)
367
367
368 changes_details = Optional.extract(details)
368 changes_details = Optional.extract(details)
369 _changes_details_types = ['basic', 'extended', 'full']
369 _changes_details_types = ['basic', 'extended', 'full']
370 if changes_details not in _changes_details_types:
370 if changes_details not in _changes_details_types:
371 raise JSONRPCError(
371 raise JSONRPCError(
372 'ret_type must be one of %s' % (
372 'ret_type must be one of %s' % (
373 ','.join(_changes_details_types)))
373 ','.join(_changes_details_types)))
374
374
375 limit = int(limit)
375 limit = int(limit)
376 pre_load = ['author', 'branch', 'date', 'message', 'parents',
376 pre_load = ['author', 'branch', 'date', 'message', 'parents',
377 'status', '_commit', '_file_paths']
377 'status', '_commit', '_file_paths']
378
378
379 vcs_repo = repo.scm_instance()
379 vcs_repo = repo.scm_instance()
380 # SVN needs a special case to distinguish its index and commit id
380 # SVN needs a special case to distinguish its index and commit id
381 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
381 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
382 start_rev = vcs_repo.commit_ids[0]
382 start_rev = vcs_repo.commit_ids[0]
383
383
384 try:
384 try:
385 commits = vcs_repo.get_commits(
385 commits = vcs_repo.get_commits(
386 start_id=start_rev, pre_load=pre_load, translate_tags=False)
386 start_id=start_rev, pre_load=pre_load, translate_tags=False)
387 except TypeError as e:
387 except TypeError as e:
388 raise JSONRPCError(safe_str(e))
388 raise JSONRPCError(safe_str(e))
389 except Exception:
389 except Exception:
390 log.exception('Fetching of commits failed')
390 log.exception('Fetching of commits failed')
391 raise JSONRPCError('Error occurred during commit fetching')
391 raise JSONRPCError('Error occurred during commit fetching')
392
392
393 ret = []
393 ret = []
394 for cnt, commit in enumerate(commits):
394 for cnt, commit in enumerate(commits):
395 if cnt >= limit != -1:
395 if cnt >= limit != -1:
396 break
396 break
397 _cs_json = commit.__json__()
397 _cs_json = commit.__json__()
398 _cs_json['diff'] = build_commit_data(commit, changes_details)
398 _cs_json['diff'] = build_commit_data(commit, changes_details)
399 if changes_details == 'full':
399 if changes_details == 'full':
400 _cs_json['refs'] = {
400 _cs_json['refs'] = {
401 'branches': [commit.branch],
401 'branches': [commit.branch],
402 'bookmarks': getattr(commit, 'bookmarks', []),
402 'bookmarks': getattr(commit, 'bookmarks', []),
403 'tags': commit.tags
403 'tags': commit.tags
404 }
404 }
405 ret.append(_cs_json)
405 ret.append(_cs_json)
406 return ret
406 return ret
407
407
408
408
409 @jsonrpc_method()
409 @jsonrpc_method()
410 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
410 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
411 ret_type=Optional('all'), details=Optional('basic'),
411 ret_type=Optional('all'), details=Optional('basic'),
412 max_file_bytes=Optional(None)):
412 max_file_bytes=Optional(None)):
413 """
413 """
414 Returns a list of nodes and children in a flat list for a given
414 Returns a list of nodes and children in a flat list for a given
415 path at given revision.
415 path at given revision.
416
416
417 It's possible to specify ret_type to show only `files` or `dirs`.
417 It's possible to specify ret_type to show only `files` or `dirs`.
418
418
419 This command can only be run using an |authtoken| with admin rights,
419 This command can only be run using an |authtoken| with admin rights,
420 or users with at least read rights to |repos|.
420 or users with at least read rights to |repos|.
421
421
422 :param apiuser: This is filled automatically from the |authtoken|.
422 :param apiuser: This is filled automatically from the |authtoken|.
423 :type apiuser: AuthUser
423 :type apiuser: AuthUser
424 :param repoid: The repository name or repository ID.
424 :param repoid: The repository name or repository ID.
425 :type repoid: str or int
425 :type repoid: str or int
426 :param revision: The revision for which listing should be done.
426 :param revision: The revision for which listing should be done.
427 :type revision: str
427 :type revision: str
428 :param root_path: The path from which to start displaying.
428 :param root_path: The path from which to start displaying.
429 :type root_path: str
429 :type root_path: str
430 :param ret_type: Set the return type. Valid options are
430 :param ret_type: Set the return type. Valid options are
431 ``all`` (default), ``files`` and ``dirs``.
431 ``all`` (default), ``files`` and ``dirs``.
432 :type ret_type: Optional(str)
432 :type ret_type: Optional(str)
433 :param details: Returns extended information about nodes, such as
433 :param details: Returns extended information about nodes, such as
434 md5, binary, and or content.
434 md5, binary, and or content.
435 The valid options are ``basic`` and ``full``.
435 The valid options are ``basic`` and ``full``.
436 :type details: Optional(str)
436 :type details: Optional(str)
437 :param max_file_bytes: Only return file content under this file size bytes
437 :param max_file_bytes: Only return file content under this file size bytes
438 :type details: Optional(int)
438 :type details: Optional(int)
439
439
440 Example output:
440 Example output:
441
441
442 .. code-block:: bash
442 .. code-block:: bash
443
443
444 id : <id_given_in_input>
444 id : <id_given_in_input>
445 result: [
445 result: [
446 {
446 {
447 "binary": false,
447 "binary": false,
448 "content": "File line",
448 "content": "File line",
449 "extension": "md",
449 "extension": "md",
450 "lines": 2,
450 "lines": 2,
451 "md5": "059fa5d29b19c0657e384749480f6422",
451 "md5": "059fa5d29b19c0657e384749480f6422",
452 "mimetype": "text/x-minidsrc",
452 "mimetype": "text/x-minidsrc",
453 "name": "file.md",
453 "name": "file.md",
454 "size": 580,
454 "size": 580,
455 "type": "file"
455 "type": "file"
456 },
456 },
457 ...
457 ...
458 ]
458 ]
459 error: null
459 error: null
460 """
460 """
461
461
462 repo = get_repo_or_error(repoid)
462 repo = get_repo_or_error(repoid)
463 if not has_superadmin_permission(apiuser):
463 if not has_superadmin_permission(apiuser):
464 _perms = ('repository.admin', 'repository.write', 'repository.read',)
464 _perms = ('repository.admin', 'repository.write', 'repository.read',)
465 validate_repo_permissions(apiuser, repoid, repo, _perms)
465 validate_repo_permissions(apiuser, repoid, repo, _perms)
466
466
467 ret_type = Optional.extract(ret_type)
467 ret_type = Optional.extract(ret_type)
468 details = Optional.extract(details)
468 details = Optional.extract(details)
469 _extended_types = ['basic', 'full']
469 _extended_types = ['basic', 'full']
470 if details not in _extended_types:
470 if details not in _extended_types:
471 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
471 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
472 extended_info = False
472 extended_info = False
473 content = False
473 content = False
474 if details == 'basic':
474 if details == 'basic':
475 extended_info = True
475 extended_info = True
476
476
477 if details == 'full':
477 if details == 'full':
478 extended_info = content = True
478 extended_info = content = True
479
479
480 _map = {}
480 _map = {}
481 try:
481 try:
482 # check if repo is not empty by any chance, skip quicker if it is.
482 # check if repo is not empty by any chance, skip quicker if it is.
483 _scm = repo.scm_instance()
483 _scm = repo.scm_instance()
484 if _scm.is_empty():
484 if _scm.is_empty():
485 return []
485 return []
486
486
487 _d, _f = ScmModel().get_nodes(
487 _d, _f = ScmModel().get_nodes(
488 repo, revision, root_path, flat=False,
488 repo, revision, root_path, flat=False,
489 extended_info=extended_info, content=content,
489 extended_info=extended_info, content=content,
490 max_file_bytes=max_file_bytes)
490 max_file_bytes=max_file_bytes)
491 _map = {
491 _map = {
492 'all': _d + _f,
492 'all': _d + _f,
493 'files': _f,
493 'files': _f,
494 'dirs': _d,
494 'dirs': _d,
495 }
495 }
496 return _map[ret_type]
496 return _map[ret_type]
497 except KeyError:
497 except KeyError:
498 raise JSONRPCError(
498 raise JSONRPCError(
499 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
499 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
500 except Exception:
500 except Exception:
501 log.exception("Exception occurred while trying to get repo nodes")
501 log.exception("Exception occurred while trying to get repo nodes")
502 raise JSONRPCError(
502 raise JSONRPCError(
503 'failed to get repo: `%s` nodes' % repo.repo_name
503 'failed to get repo: `%s` nodes' % repo.repo_name
504 )
504 )
505
505
506
506
507 @jsonrpc_method()
507 @jsonrpc_method()
508 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
508 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
509 max_file_bytes=Optional(None), details=Optional('basic'),
509 max_file_bytes=Optional(None), details=Optional('basic'),
510 cache=Optional(True)):
510 cache=Optional(True)):
511 """
511 """
512 Returns a single file from repository at given revision.
512 Returns a single file from repository at given revision.
513
513
514 This command can only be run using an |authtoken| with admin rights,
514 This command can only be run using an |authtoken| with admin rights,
515 or users with at least read rights to |repos|.
515 or users with at least read rights to |repos|.
516
516
517 :param apiuser: This is filled automatically from the |authtoken|.
517 :param apiuser: This is filled automatically from the |authtoken|.
518 :type apiuser: AuthUser
518 :type apiuser: AuthUser
519 :param repoid: The repository name or repository ID.
519 :param repoid: The repository name or repository ID.
520 :type repoid: str or int
520 :type repoid: str or int
521 :param commit_id: The revision for which listing should be done.
521 :param commit_id: The revision for which listing should be done.
522 :type commit_id: str
522 :type commit_id: str
523 :param file_path: The path from which to start displaying.
523 :param file_path: The path from which to start displaying.
524 :type file_path: str
524 :type file_path: str
525 :param details: Returns different set of information about nodes.
525 :param details: Returns different set of information about nodes.
526 The valid options are ``minimal`` ``basic`` and ``full``.
526 The valid options are ``minimal`` ``basic`` and ``full``.
527 :type details: Optional(str)
527 :type details: Optional(str)
528 :param max_file_bytes: Only return file content under this file size bytes
528 :param max_file_bytes: Only return file content under this file size bytes
529 :type max_file_bytes: Optional(int)
529 :type max_file_bytes: Optional(int)
530 :param cache: Use internal caches for fetching files. If disabled fetching
530 :param cache: Use internal caches for fetching files. If disabled fetching
531 files is slower but more memory efficient
531 files is slower but more memory efficient
532 :type cache: Optional(bool)
532 :type cache: Optional(bool)
533
533
534 Example output:
534 Example output:
535
535
536 .. code-block:: bash
536 .. code-block:: bash
537
537
538 id : <id_given_in_input>
538 id : <id_given_in_input>
539 result: {
539 result: {
540 "binary": false,
540 "binary": false,
541 "extension": "py",
541 "extension": "py",
542 "lines": 35,
542 "lines": 35,
543 "content": "....",
543 "content": "....",
544 "md5": "76318336366b0f17ee249e11b0c99c41",
544 "md5": "76318336366b0f17ee249e11b0c99c41",
545 "mimetype": "text/x-python",
545 "mimetype": "text/x-python",
546 "name": "python.py",
546 "name": "python.py",
547 "size": 817,
547 "size": 817,
548 "type": "file",
548 "type": "file",
549 }
549 }
550 error: null
550 error: null
551 """
551 """
552
552
553 repo = get_repo_or_error(repoid)
553 repo = get_repo_or_error(repoid)
554 if not has_superadmin_permission(apiuser):
554 if not has_superadmin_permission(apiuser):
555 _perms = ('repository.admin', 'repository.write', 'repository.read',)
555 _perms = ('repository.admin', 'repository.write', 'repository.read',)
556 validate_repo_permissions(apiuser, repoid, repo, _perms)
556 validate_repo_permissions(apiuser, repoid, repo, _perms)
557
557
558 cache = Optional.extract(cache, binary=True)
558 cache = Optional.extract(cache, binary=True)
559 details = Optional.extract(details)
559 details = Optional.extract(details)
560 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
560 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
561 if details not in _extended_types:
561 if details not in _extended_types:
562 raise JSONRPCError(
562 raise JSONRPCError(
563 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
563 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
564 extended_info = False
564 extended_info = False
565 content = False
565 content = False
566
566
567 if details == 'minimal':
567 if details == 'minimal':
568 extended_info = False
568 extended_info = False
569
569
570 elif details == 'basic':
570 elif details == 'basic':
571 extended_info = True
571 extended_info = True
572
572
573 elif details == 'full':
573 elif details == 'full':
574 extended_info = content = True
574 extended_info = content = True
575
575
576 try:
576 try:
577 # check if repo is not empty by any chance, skip quicker if it is.
577 # check if repo is not empty by any chance, skip quicker if it is.
578 _scm = repo.scm_instance()
578 _scm = repo.scm_instance()
579 if _scm.is_empty():
579 if _scm.is_empty():
580 return None
580 return None
581
581
582 node = ScmModel().get_node(
582 node = ScmModel().get_node(
583 repo, commit_id, file_path, extended_info=extended_info,
583 repo, commit_id, file_path, extended_info=extended_info,
584 content=content, max_file_bytes=max_file_bytes, cache=cache)
584 content=content, max_file_bytes=max_file_bytes, cache=cache)
585 except NodeDoesNotExistError:
585 except NodeDoesNotExistError:
586 raise JSONRPCError('There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
586 raise JSONRPCError('There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
587 repo.repo_name, file_path, commit_id))
587 repo.repo_name, file_path, commit_id))
588 except Exception:
588 except Exception:
589 log.exception("Exception occurred while trying to get repo %s file",
589 log.exception("Exception occurred while trying to get repo %s file",
590 repo.repo_name)
590 repo.repo_name)
591 raise JSONRPCError('failed to get repo: `{}` file at path {}'.format(
591 raise JSONRPCError('failed to get repo: `{}` file at path {}'.format(
592 repo.repo_name, file_path))
592 repo.repo_name, file_path))
593
593
594 return node
594 return node
595
595
596
596
597 @jsonrpc_method()
597 @jsonrpc_method()
598 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
598 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
599 """
599 """
600 Returns a list of tree nodes for path at given revision. This api is built
600 Returns a list of tree nodes for path at given revision. This api is built
601 strictly for usage in full text search building, and shouldn't be consumed
601 strictly for usage in full text search building, and shouldn't be consumed
602
602
603 This command can only be run using an |authtoken| with admin rights,
603 This command can only be run using an |authtoken| with admin rights,
604 or users with at least read rights to |repos|.
604 or users with at least read rights to |repos|.
605
605
606 """
606 """
607
607
608 repo = get_repo_or_error(repoid)
608 repo = get_repo_or_error(repoid)
609 if not has_superadmin_permission(apiuser):
609 if not has_superadmin_permission(apiuser):
610 _perms = ('repository.admin', 'repository.write', 'repository.read',)
610 _perms = ('repository.admin', 'repository.write', 'repository.read',)
611 validate_repo_permissions(apiuser, repoid, repo, _perms)
611 validate_repo_permissions(apiuser, repoid, repo, _perms)
612
612
613 repo_id = repo.repo_id
613 repo_id = repo.repo_id
614 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
614 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
615 cache_on = cache_seconds > 0
615 cache_on = cache_seconds > 0
616
616
617 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
617 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
618 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
618 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
619
619
620 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
620 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
621 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
621 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
622
622
623 try:
623 try:
624 # check if repo is not empty by any chance, skip quicker if it is.
624 # check if repo is not empty by any chance, skip quicker if it is.
625 _scm = repo.scm_instance()
625 _scm = repo.scm_instance()
626 if _scm.is_empty():
626 if _scm.is_empty():
627 return []
627 return []
628 except RepositoryError:
628 except RepositoryError:
629 log.exception("Exception occurred while trying to get repo nodes")
629 log.exception("Exception occurred while trying to get repo nodes")
630 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
630 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
631
631
632 try:
632 try:
633 # we need to resolve commit_id to a FULL sha for cache to work correctly.
633 # we need to resolve commit_id to a FULL sha for cache to work correctly.
634 # sending 'master' is a pointer that needs to be translated to current commit.
634 # sending 'master' is a pointer that needs to be translated to current commit.
635 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
635 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
636 log.debug(
636 log.debug(
637 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
637 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
638 'with caching: %s[TTL: %ss]' % (
638 'with caching: %s[TTL: %ss]' % (
639 repo_id, commit_id, cache_on, cache_seconds or 0))
639 repo_id, commit_id, cache_on, cache_seconds or 0))
640
640
641 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
641 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
642 return tree_files
642 return tree_files
643
643
644 except Exception:
644 except Exception:
645 log.exception("Exception occurred while trying to get repo nodes")
645 log.exception("Exception occurred while trying to get repo nodes")
646 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
646 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
647
647
648
648
649 @jsonrpc_method()
649 @jsonrpc_method()
650 def get_repo_refs(request, apiuser, repoid):
650 def get_repo_refs(request, apiuser, repoid):
651 """
651 """
652 Returns a dictionary of current references. It returns
652 Returns a dictionary of current references. It returns
653 bookmarks, branches, closed_branches, and tags for given repository
653 bookmarks, branches, closed_branches, and tags for given repository
654
654
655 It's possible to specify ret_type to show only `files` or `dirs`.
655 It's possible to specify ret_type to show only `files` or `dirs`.
656
656
657 This command can only be run using an |authtoken| with admin rights,
657 This command can only be run using an |authtoken| with admin rights,
658 or users with at least read rights to |repos|.
658 or users with at least read rights to |repos|.
659
659
660 :param apiuser: This is filled automatically from the |authtoken|.
660 :param apiuser: This is filled automatically from the |authtoken|.
661 :type apiuser: AuthUser
661 :type apiuser: AuthUser
662 :param repoid: The repository name or repository ID.
662 :param repoid: The repository name or repository ID.
663 :type repoid: str or int
663 :type repoid: str or int
664
664
665 Example output:
665 Example output:
666
666
667 .. code-block:: bash
667 .. code-block:: bash
668
668
669 id : <id_given_in_input>
669 id : <id_given_in_input>
670 "result": {
670 "result": {
671 "bookmarks": {
671 "bookmarks": {
672 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
672 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
673 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
673 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
674 },
674 },
675 "branches": {
675 "branches": {
676 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
676 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
677 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
677 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
678 },
678 },
679 "branches_closed": {},
679 "branches_closed": {},
680 "tags": {
680 "tags": {
681 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
681 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
682 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
682 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
683 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
683 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
684 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
684 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
685 }
685 }
686 }
686 }
687 error: null
687 error: null
688 """
688 """
689
689
690 repo = get_repo_or_error(repoid)
690 repo = get_repo_or_error(repoid)
691 if not has_superadmin_permission(apiuser):
691 if not has_superadmin_permission(apiuser):
692 _perms = ('repository.admin', 'repository.write', 'repository.read',)
692 _perms = ('repository.admin', 'repository.write', 'repository.read',)
693 validate_repo_permissions(apiuser, repoid, repo, _perms)
693 validate_repo_permissions(apiuser, repoid, repo, _perms)
694
694
695 try:
695 try:
696 # check if repo is not empty by any chance, skip quicker if it is.
696 # check if repo is not empty by any chance, skip quicker if it is.
697 vcs_instance = repo.scm_instance()
697 vcs_instance = repo.scm_instance()
698 refs = vcs_instance.refs()
698 refs = vcs_instance.refs()
699 return refs
699 return refs
700 except Exception:
700 except Exception:
701 log.exception("Exception occurred while trying to get repo refs")
701 log.exception("Exception occurred while trying to get repo refs")
702 raise JSONRPCError(
702 raise JSONRPCError(
703 'failed to get repo: `%s` references' % repo.repo_name
703 'failed to get repo: `%s` references' % repo.repo_name
704 )
704 )
705
705
706
706
707 @jsonrpc_method()
707 @jsonrpc_method()
708 def create_repo(
708 def create_repo(
709 request, apiuser, repo_name, repo_type,
709 request, apiuser, repo_name, repo_type,
710 owner=Optional(OAttr('apiuser')),
710 owner=Optional(OAttr('apiuser')),
711 description=Optional(''),
711 description=Optional(''),
712 private=Optional(False),
712 private=Optional(False),
713 clone_uri=Optional(None),
713 clone_uri=Optional(None),
714 push_uri=Optional(None),
714 push_uri=Optional(None),
715 landing_rev=Optional(None),
715 landing_rev=Optional(None),
716 enable_statistics=Optional(False),
716 enable_statistics=Optional(False),
717 enable_locking=Optional(False),
717 enable_locking=Optional(False),
718 enable_downloads=Optional(False),
718 enable_downloads=Optional(False),
719 copy_permissions=Optional(False)):
719 copy_permissions=Optional(False)):
720 """
720 """
721 Creates a repository.
721 Creates a repository.
722
722
723 * If the repository name contains "/", repository will be created inside
723 * If the repository name contains "/", repository will be created inside
724 a repository group or nested repository groups
724 a repository group or nested repository groups
725
725
726 For example "foo/bar/repo1" will create |repo| called "repo1" inside
726 For example "foo/bar/repo1" will create |repo| called "repo1" inside
727 group "foo/bar". You have to have permissions to access and write to
727 group "foo/bar". You have to have permissions to access and write to
728 the last repository group ("bar" in this example)
728 the last repository group ("bar" in this example)
729
729
730 This command can only be run using an |authtoken| with at least
730 This command can only be run using an |authtoken| with at least
731 permissions to create repositories, or write permissions to
731 permissions to create repositories, or write permissions to
732 parent repository groups.
732 parent repository groups.
733
733
734 :param apiuser: This is filled automatically from the |authtoken|.
734 :param apiuser: This is filled automatically from the |authtoken|.
735 :type apiuser: AuthUser
735 :type apiuser: AuthUser
736 :param repo_name: Set the repository name.
736 :param repo_name: Set the repository name.
737 :type repo_name: str
737 :type repo_name: str
738 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
738 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
739 :type repo_type: str
739 :type repo_type: str
740 :param owner: user_id or username
740 :param owner: user_id or username
741 :type owner: Optional(str)
741 :type owner: Optional(str)
742 :param description: Set the repository description.
742 :param description: Set the repository description.
743 :type description: Optional(str)
743 :type description: Optional(str)
744 :param private: set repository as private
744 :param private: set repository as private
745 :type private: bool
745 :type private: bool
746 :param clone_uri: set clone_uri
746 :param clone_uri: set clone_uri
747 :type clone_uri: str
747 :type clone_uri: str
748 :param push_uri: set push_uri
748 :param push_uri: set push_uri
749 :type push_uri: str
749 :type push_uri: str
750 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
750 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
751 :type landing_rev: str
751 :type landing_rev: str
752 :param enable_locking:
752 :param enable_locking:
753 :type enable_locking: bool
753 :type enable_locking: bool
754 :param enable_downloads:
754 :param enable_downloads:
755 :type enable_downloads: bool
755 :type enable_downloads: bool
756 :param enable_statistics:
756 :param enable_statistics:
757 :type enable_statistics: bool
757 :type enable_statistics: bool
758 :param copy_permissions: Copy permission from group in which the
758 :param copy_permissions: Copy permission from group in which the
759 repository is being created.
759 repository is being created.
760 :type copy_permissions: bool
760 :type copy_permissions: bool
761
761
762
762
763 Example output:
763 Example output:
764
764
765 .. code-block:: bash
765 .. code-block:: bash
766
766
767 id : <id_given_in_input>
767 id : <id_given_in_input>
768 result: {
768 result: {
769 "msg": "Created new repository `<reponame>`",
769 "msg": "Created new repository `<reponame>`",
770 "success": true,
770 "success": true,
771 "task": "<celery task id or None if done sync>"
771 "task": "<celery task id or None if done sync>"
772 }
772 }
773 error: null
773 error: null
774
774
775
775
776 Example error output:
776 Example error output:
777
777
778 .. code-block:: bash
778 .. code-block:: bash
779
779
780 id : <id_given_in_input>
780 id : <id_given_in_input>
781 result : null
781 result : null
782 error : {
782 error : {
783 'failed to create repository `<repo_name>`'
783 'failed to create repository `<repo_name>`'
784 }
784 }
785
785
786 """
786 """
787
787
788 owner = validate_set_owner_permissions(apiuser, owner)
788 owner = validate_set_owner_permissions(apiuser, owner)
789
789
790 description = Optional.extract(description)
790 description = Optional.extract(description)
791 copy_permissions = Optional.extract(copy_permissions)
791 copy_permissions = Optional.extract(copy_permissions)
792 clone_uri = Optional.extract(clone_uri)
792 clone_uri = Optional.extract(clone_uri)
793 push_uri = Optional.extract(push_uri)
793 push_uri = Optional.extract(push_uri)
794
794
795 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
795 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
796 if isinstance(private, Optional):
796 if isinstance(private, Optional):
797 private = defs.get('repo_private') or Optional.extract(private)
797 private = defs.get('repo_private') or Optional.extract(private)
798 if isinstance(repo_type, Optional):
798 if isinstance(repo_type, Optional):
799 repo_type = defs.get('repo_type')
799 repo_type = defs.get('repo_type')
800 if isinstance(enable_statistics, Optional):
800 if isinstance(enable_statistics, Optional):
801 enable_statistics = defs.get('repo_enable_statistics')
801 enable_statistics = defs.get('repo_enable_statistics')
802 if isinstance(enable_locking, Optional):
802 if isinstance(enable_locking, Optional):
803 enable_locking = defs.get('repo_enable_locking')
803 enable_locking = defs.get('repo_enable_locking')
804 if isinstance(enable_downloads, Optional):
804 if isinstance(enable_downloads, Optional):
805 enable_downloads = defs.get('repo_enable_downloads')
805 enable_downloads = defs.get('repo_enable_downloads')
806
806
807 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
807 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
808 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
808 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
809 ref_choices = list(set(ref_choices + [landing_ref]))
809 ref_choices = list(set(ref_choices + [landing_ref]))
810
810
811 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
811 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
812
812
813 schema = repo_schema.RepoSchema().bind(
813 schema = repo_schema.RepoSchema().bind(
814 repo_type_options=rhodecode.BACKENDS.keys(),
814 repo_type_options=rhodecode.BACKENDS.keys(),
815 repo_ref_options=ref_choices,
815 repo_ref_options=ref_choices,
816 repo_type=repo_type,
816 repo_type=repo_type,
817 # user caller
817 # user caller
818 user=apiuser)
818 user=apiuser)
819
819
820 try:
820 try:
821 schema_data = schema.deserialize(dict(
821 schema_data = schema.deserialize(dict(
822 repo_name=repo_name,
822 repo_name=repo_name,
823 repo_type=repo_type,
823 repo_type=repo_type,
824 repo_owner=owner.username,
824 repo_owner=owner.username,
825 repo_description=description,
825 repo_description=description,
826 repo_landing_commit_ref=landing_commit_ref,
826 repo_landing_commit_ref=landing_commit_ref,
827 repo_clone_uri=clone_uri,
827 repo_clone_uri=clone_uri,
828 repo_push_uri=push_uri,
828 repo_push_uri=push_uri,
829 repo_private=private,
829 repo_private=private,
830 repo_copy_permissions=copy_permissions,
830 repo_copy_permissions=copy_permissions,
831 repo_enable_statistics=enable_statistics,
831 repo_enable_statistics=enable_statistics,
832 repo_enable_downloads=enable_downloads,
832 repo_enable_downloads=enable_downloads,
833 repo_enable_locking=enable_locking))
833 repo_enable_locking=enable_locking))
834 except validation_schema.Invalid as err:
834 except validation_schema.Invalid as err:
835 raise JSONRPCValidationError(colander_exc=err)
835 raise JSONRPCValidationError(colander_exc=err)
836
836
837 try:
837 try:
838 data = {
838 data = {
839 'owner': owner,
839 'owner': owner,
840 'repo_name': schema_data['repo_group']['repo_name_without_group'],
840 'repo_name': schema_data['repo_group']['repo_name_without_group'],
841 'repo_name_full': schema_data['repo_name'],
841 'repo_name_full': schema_data['repo_name'],
842 'repo_group': schema_data['repo_group']['repo_group_id'],
842 'repo_group': schema_data['repo_group']['repo_group_id'],
843 'repo_type': schema_data['repo_type'],
843 'repo_type': schema_data['repo_type'],
844 'repo_description': schema_data['repo_description'],
844 'repo_description': schema_data['repo_description'],
845 'repo_private': schema_data['repo_private'],
845 'repo_private': schema_data['repo_private'],
846 'clone_uri': schema_data['repo_clone_uri'],
846 'clone_uri': schema_data['repo_clone_uri'],
847 'push_uri': schema_data['repo_push_uri'],
847 'push_uri': schema_data['repo_push_uri'],
848 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
848 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
849 'enable_statistics': schema_data['repo_enable_statistics'],
849 'enable_statistics': schema_data['repo_enable_statistics'],
850 'enable_locking': schema_data['repo_enable_locking'],
850 'enable_locking': schema_data['repo_enable_locking'],
851 'enable_downloads': schema_data['repo_enable_downloads'],
851 'enable_downloads': schema_data['repo_enable_downloads'],
852 'repo_copy_permissions': schema_data['repo_copy_permissions'],
852 'repo_copy_permissions': schema_data['repo_copy_permissions'],
853 }
853 }
854
854
855 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
855 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
856 task_id = get_task_id(task)
856 task_id = get_task_id(task)
857 # no commit, it's done in RepoModel, or async via celery
857 # no commit, it's done in RepoModel, or async via celery
858 return {
858 return {
859 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
859 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
860 'success': True, # cannot return the repo data here since fork
860 'success': True, # cannot return the repo data here since fork
861 # can be done async
861 # can be done async
862 'task': task_id
862 'task': task_id
863 }
863 }
864 except Exception:
864 except Exception:
865 log.exception(
865 log.exception(
866 u"Exception while trying to create the repository %s",
866 u"Exception while trying to create the repository %s",
867 schema_data['repo_name'])
867 schema_data['repo_name'])
868 raise JSONRPCError(
868 raise JSONRPCError(
869 'failed to create repository `%s`' % (schema_data['repo_name'],))
869 'failed to create repository `%s`' % (schema_data['repo_name'],))
870
870
871
871
872 @jsonrpc_method()
872 @jsonrpc_method()
873 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
873 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
874 description=Optional('')):
874 description=Optional('')):
875 """
875 """
876 Adds an extra field to a repository.
876 Adds an extra field to a repository.
877
877
878 This command can only be run using an |authtoken| with at least
878 This command can only be run using an |authtoken| with at least
879 write permissions to the |repo|.
879 write permissions to the |repo|.
880
880
881 :param apiuser: This is filled automatically from the |authtoken|.
881 :param apiuser: This is filled automatically from the |authtoken|.
882 :type apiuser: AuthUser
882 :type apiuser: AuthUser
883 :param repoid: Set the repository name or repository id.
883 :param repoid: Set the repository name or repository id.
884 :type repoid: str or int
884 :type repoid: str or int
885 :param key: Create a unique field key for this repository.
885 :param key: Create a unique field key for this repository.
886 :type key: str
886 :type key: str
887 :param label:
887 :param label:
888 :type label: Optional(str)
888 :type label: Optional(str)
889 :param description:
889 :param description:
890 :type description: Optional(str)
890 :type description: Optional(str)
891 """
891 """
892 repo = get_repo_or_error(repoid)
892 repo = get_repo_or_error(repoid)
893 if not has_superadmin_permission(apiuser):
893 if not has_superadmin_permission(apiuser):
894 _perms = ('repository.admin',)
894 _perms = ('repository.admin',)
895 validate_repo_permissions(apiuser, repoid, repo, _perms)
895 validate_repo_permissions(apiuser, repoid, repo, _perms)
896
896
897 label = Optional.extract(label) or key
897 label = Optional.extract(label) or key
898 description = Optional.extract(description)
898 description = Optional.extract(description)
899
899
900 field = RepositoryField.get_by_key_name(key, repo)
900 field = RepositoryField.get_by_key_name(key, repo)
901 if field:
901 if field:
902 raise JSONRPCError('Field with key '
902 raise JSONRPCError('Field with key '
903 '`%s` exists for repo `%s`' % (key, repoid))
903 '`%s` exists for repo `%s`' % (key, repoid))
904
904
905 try:
905 try:
906 RepoModel().add_repo_field(repo, key, field_label=label,
906 RepoModel().add_repo_field(repo, key, field_label=label,
907 field_desc=description)
907 field_desc=description)
908 Session().commit()
908 Session().commit()
909 return {
909 return {
910 'msg': "Added new repository field `%s`" % (key,),
910 'msg': "Added new repository field `%s`" % (key,),
911 'success': True,
911 'success': True,
912 }
912 }
913 except Exception:
913 except Exception:
914 log.exception("Exception occurred while trying to add field to repo")
914 log.exception("Exception occurred while trying to add field to repo")
915 raise JSONRPCError(
915 raise JSONRPCError(
916 'failed to create new field for repository `%s`' % (repoid,))
916 'failed to create new field for repository `%s`' % (repoid,))
917
917
918
918
919 @jsonrpc_method()
919 @jsonrpc_method()
920 def remove_field_from_repo(request, apiuser, repoid, key):
920 def remove_field_from_repo(request, apiuser, repoid, key):
921 """
921 """
922 Removes an extra field from a repository.
922 Removes an extra field from a repository.
923
923
924 This command can only be run using an |authtoken| with at least
924 This command can only be run using an |authtoken| with at least
925 write permissions to the |repo|.
925 write permissions to the |repo|.
926
926
927 :param apiuser: This is filled automatically from the |authtoken|.
927 :param apiuser: This is filled automatically from the |authtoken|.
928 :type apiuser: AuthUser
928 :type apiuser: AuthUser
929 :param repoid: Set the repository name or repository ID.
929 :param repoid: Set the repository name or repository ID.
930 :type repoid: str or int
930 :type repoid: str or int
931 :param key: Set the unique field key for this repository.
931 :param key: Set the unique field key for this repository.
932 :type key: str
932 :type key: str
933 """
933 """
934
934
935 repo = get_repo_or_error(repoid)
935 repo = get_repo_or_error(repoid)
936 if not has_superadmin_permission(apiuser):
936 if not has_superadmin_permission(apiuser):
937 _perms = ('repository.admin',)
937 _perms = ('repository.admin',)
938 validate_repo_permissions(apiuser, repoid, repo, _perms)
938 validate_repo_permissions(apiuser, repoid, repo, _perms)
939
939
940 field = RepositoryField.get_by_key_name(key, repo)
940 field = RepositoryField.get_by_key_name(key, repo)
941 if not field:
941 if not field:
942 raise JSONRPCError('Field with key `%s` does not '
942 raise JSONRPCError('Field with key `%s` does not '
943 'exists for repo `%s`' % (key, repoid))
943 'exists for repo `%s`' % (key, repoid))
944
944
945 try:
945 try:
946 RepoModel().delete_repo_field(repo, field_key=key)
946 RepoModel().delete_repo_field(repo, field_key=key)
947 Session().commit()
947 Session().commit()
948 return {
948 return {
949 'msg': "Deleted repository field `%s`" % (key,),
949 'msg': "Deleted repository field `%s`" % (key,),
950 'success': True,
950 'success': True,
951 }
951 }
952 except Exception:
952 except Exception:
953 log.exception(
953 log.exception(
954 "Exception occurred while trying to delete field from repo")
954 "Exception occurred while trying to delete field from repo")
955 raise JSONRPCError(
955 raise JSONRPCError(
956 'failed to delete field for repository `%s`' % (repoid,))
956 'failed to delete field for repository `%s`' % (repoid,))
957
957
958
958
959 @jsonrpc_method()
959 @jsonrpc_method()
960 def update_repo(
960 def update_repo(
961 request, apiuser, repoid, repo_name=Optional(None),
961 request, apiuser, repoid, repo_name=Optional(None),
962 owner=Optional(OAttr('apiuser')), description=Optional(''),
962 owner=Optional(OAttr('apiuser')), description=Optional(''),
963 private=Optional(False),
963 private=Optional(False),
964 clone_uri=Optional(None), push_uri=Optional(None),
964 clone_uri=Optional(None), push_uri=Optional(None),
965 landing_rev=Optional(None), fork_of=Optional(None),
965 landing_rev=Optional(None), fork_of=Optional(None),
966 enable_statistics=Optional(False),
966 enable_statistics=Optional(False),
967 enable_locking=Optional(False),
967 enable_locking=Optional(False),
968 enable_downloads=Optional(False), fields=Optional('')):
968 enable_downloads=Optional(False), fields=Optional('')):
969 """
969 """
970 Updates a repository with the given information.
970 Updates a repository with the given information.
971
971
972 This command can only be run using an |authtoken| with at least
972 This command can only be run using an |authtoken| with at least
973 admin permissions to the |repo|.
973 admin permissions to the |repo|.
974
974
975 * If the repository name contains "/", repository will be updated
975 * If the repository name contains "/", repository will be updated
976 accordingly with a repository group or nested repository groups
976 accordingly with a repository group or nested repository groups
977
977
978 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
978 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
979 called "repo-test" and place it inside group "foo/bar".
979 called "repo-test" and place it inside group "foo/bar".
980 You have to have permissions to access and write to the last repository
980 You have to have permissions to access and write to the last repository
981 group ("bar" in this example)
981 group ("bar" in this example)
982
982
983 :param apiuser: This is filled automatically from the |authtoken|.
983 :param apiuser: This is filled automatically from the |authtoken|.
984 :type apiuser: AuthUser
984 :type apiuser: AuthUser
985 :param repoid: repository name or repository ID.
985 :param repoid: repository name or repository ID.
986 :type repoid: str or int
986 :type repoid: str or int
987 :param repo_name: Update the |repo| name, including the
987 :param repo_name: Update the |repo| name, including the
988 repository group it's in.
988 repository group it's in.
989 :type repo_name: str
989 :type repo_name: str
990 :param owner: Set the |repo| owner.
990 :param owner: Set the |repo| owner.
991 :type owner: str
991 :type owner: str
992 :param fork_of: Set the |repo| as fork of another |repo|.
992 :param fork_of: Set the |repo| as fork of another |repo|.
993 :type fork_of: str
993 :type fork_of: str
994 :param description: Update the |repo| description.
994 :param description: Update the |repo| description.
995 :type description: str
995 :type description: str
996 :param private: Set the |repo| as private. (True | False)
996 :param private: Set the |repo| as private. (True | False)
997 :type private: bool
997 :type private: bool
998 :param clone_uri: Update the |repo| clone URI.
998 :param clone_uri: Update the |repo| clone URI.
999 :type clone_uri: str
999 :type clone_uri: str
1000 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1000 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1001 :type landing_rev: str
1001 :type landing_rev: str
1002 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1002 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1003 :type enable_statistics: bool
1003 :type enable_statistics: bool
1004 :param enable_locking: Enable |repo| locking.
1004 :param enable_locking: Enable |repo| locking.
1005 :type enable_locking: bool
1005 :type enable_locking: bool
1006 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1006 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1007 :type enable_downloads: bool
1007 :type enable_downloads: bool
1008 :param fields: Add extra fields to the |repo|. Use the following
1008 :param fields: Add extra fields to the |repo|. Use the following
1009 example format: ``field_key=field_val,field_key2=fieldval2``.
1009 example format: ``field_key=field_val,field_key2=fieldval2``.
1010 Escape ', ' with \,
1010 Escape ', ' with \,
1011 :type fields: str
1011 :type fields: str
1012 """
1012 """
1013
1013
1014 repo = get_repo_or_error(repoid)
1014 repo = get_repo_or_error(repoid)
1015
1015
1016 include_secrets = False
1016 include_secrets = False
1017 if not has_superadmin_permission(apiuser):
1017 if not has_superadmin_permission(apiuser):
1018 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1018 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1019 else:
1019 else:
1020 include_secrets = True
1020 include_secrets = True
1021
1021
1022 updates = dict(
1022 updates = dict(
1023 repo_name=repo_name
1023 repo_name=repo_name
1024 if not isinstance(repo_name, Optional) else repo.repo_name,
1024 if not isinstance(repo_name, Optional) else repo.repo_name,
1025
1025
1026 fork_id=fork_of
1026 fork_id=fork_of
1027 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1027 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1028
1028
1029 user=owner
1029 user=owner
1030 if not isinstance(owner, Optional) else repo.user.username,
1030 if not isinstance(owner, Optional) else repo.user.username,
1031
1031
1032 repo_description=description
1032 repo_description=description
1033 if not isinstance(description, Optional) else repo.description,
1033 if not isinstance(description, Optional) else repo.description,
1034
1034
1035 repo_private=private
1035 repo_private=private
1036 if not isinstance(private, Optional) else repo.private,
1036 if not isinstance(private, Optional) else repo.private,
1037
1037
1038 clone_uri=clone_uri
1038 clone_uri=clone_uri
1039 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1039 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1040
1040
1041 push_uri=push_uri
1041 push_uri=push_uri
1042 if not isinstance(push_uri, Optional) else repo.push_uri,
1042 if not isinstance(push_uri, Optional) else repo.push_uri,
1043
1043
1044 repo_landing_rev=landing_rev
1044 repo_landing_rev=landing_rev
1045 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1045 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1046
1046
1047 repo_enable_statistics=enable_statistics
1047 repo_enable_statistics=enable_statistics
1048 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1048 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1049
1049
1050 repo_enable_locking=enable_locking
1050 repo_enable_locking=enable_locking
1051 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1051 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1052
1052
1053 repo_enable_downloads=enable_downloads
1053 repo_enable_downloads=enable_downloads
1054 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1054 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1055
1055
1056 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1056 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1057 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1057 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1058 request.translate, repo=repo)
1058 request.translate, repo=repo)
1059 ref_choices = list(set(ref_choices + [landing_ref]))
1059 ref_choices = list(set(ref_choices + [landing_ref]))
1060
1060
1061 old_values = repo.get_api_data()
1061 old_values = repo.get_api_data()
1062 repo_type = repo.repo_type
1062 repo_type = repo.repo_type
1063 schema = repo_schema.RepoSchema().bind(
1063 schema = repo_schema.RepoSchema().bind(
1064 repo_type_options=rhodecode.BACKENDS.keys(),
1064 repo_type_options=rhodecode.BACKENDS.keys(),
1065 repo_ref_options=ref_choices,
1065 repo_ref_options=ref_choices,
1066 repo_type=repo_type,
1066 repo_type=repo_type,
1067 # user caller
1067 # user caller
1068 user=apiuser,
1068 user=apiuser,
1069 old_values=old_values)
1069 old_values=old_values)
1070 try:
1070 try:
1071 schema_data = schema.deserialize(dict(
1071 schema_data = schema.deserialize(dict(
1072 # we save old value, users cannot change type
1072 # we save old value, users cannot change type
1073 repo_type=repo_type,
1073 repo_type=repo_type,
1074
1074
1075 repo_name=updates['repo_name'],
1075 repo_name=updates['repo_name'],
1076 repo_owner=updates['user'],
1076 repo_owner=updates['user'],
1077 repo_description=updates['repo_description'],
1077 repo_description=updates['repo_description'],
1078 repo_clone_uri=updates['clone_uri'],
1078 repo_clone_uri=updates['clone_uri'],
1079 repo_push_uri=updates['push_uri'],
1079 repo_push_uri=updates['push_uri'],
1080 repo_fork_of=updates['fork_id'],
1080 repo_fork_of=updates['fork_id'],
1081 repo_private=updates['repo_private'],
1081 repo_private=updates['repo_private'],
1082 repo_landing_commit_ref=updates['repo_landing_rev'],
1082 repo_landing_commit_ref=updates['repo_landing_rev'],
1083 repo_enable_statistics=updates['repo_enable_statistics'],
1083 repo_enable_statistics=updates['repo_enable_statistics'],
1084 repo_enable_downloads=updates['repo_enable_downloads'],
1084 repo_enable_downloads=updates['repo_enable_downloads'],
1085 repo_enable_locking=updates['repo_enable_locking']))
1085 repo_enable_locking=updates['repo_enable_locking']))
1086 except validation_schema.Invalid as err:
1086 except validation_schema.Invalid as err:
1087 raise JSONRPCValidationError(colander_exc=err)
1087 raise JSONRPCValidationError(colander_exc=err)
1088
1088
1089 # save validated data back into the updates dict
1089 # save validated data back into the updates dict
1090 validated_updates = dict(
1090 validated_updates = dict(
1091 repo_name=schema_data['repo_group']['repo_name_without_group'],
1091 repo_name=schema_data['repo_group']['repo_name_without_group'],
1092 repo_group=schema_data['repo_group']['repo_group_id'],
1092 repo_group=schema_data['repo_group']['repo_group_id'],
1093
1093
1094 user=schema_data['repo_owner'],
1094 user=schema_data['repo_owner'],
1095 repo_description=schema_data['repo_description'],
1095 repo_description=schema_data['repo_description'],
1096 repo_private=schema_data['repo_private'],
1096 repo_private=schema_data['repo_private'],
1097 clone_uri=schema_data['repo_clone_uri'],
1097 clone_uri=schema_data['repo_clone_uri'],
1098 push_uri=schema_data['repo_push_uri'],
1098 push_uri=schema_data['repo_push_uri'],
1099 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1099 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1100 repo_enable_statistics=schema_data['repo_enable_statistics'],
1100 repo_enable_statistics=schema_data['repo_enable_statistics'],
1101 repo_enable_locking=schema_data['repo_enable_locking'],
1101 repo_enable_locking=schema_data['repo_enable_locking'],
1102 repo_enable_downloads=schema_data['repo_enable_downloads'],
1102 repo_enable_downloads=schema_data['repo_enable_downloads'],
1103 )
1103 )
1104
1104
1105 if schema_data['repo_fork_of']:
1105 if schema_data['repo_fork_of']:
1106 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1106 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1107 validated_updates['fork_id'] = fork_repo.repo_id
1107 validated_updates['fork_id'] = fork_repo.repo_id
1108
1108
1109 # extra fields
1109 # extra fields
1110 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1110 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1111 if fields:
1111 if fields:
1112 validated_updates.update(fields)
1112 validated_updates.update(fields)
1113
1113
1114 try:
1114 try:
1115 RepoModel().update(repo, **validated_updates)
1115 RepoModel().update(repo, **validated_updates)
1116 audit_logger.store_api(
1116 audit_logger.store_api(
1117 'repo.edit', action_data={'old_data': old_values},
1117 'repo.edit', action_data={'old_data': old_values},
1118 user=apiuser, repo=repo)
1118 user=apiuser, repo=repo)
1119 Session().commit()
1119 Session().commit()
1120 return {
1120 return {
1121 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1121 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1122 'repository': repo.get_api_data(include_secrets=include_secrets)
1122 'repository': repo.get_api_data(include_secrets=include_secrets)
1123 }
1123 }
1124 except Exception:
1124 except Exception:
1125 log.exception(
1125 log.exception(
1126 u"Exception while trying to update the repository %s",
1126 u"Exception while trying to update the repository %s",
1127 repoid)
1127 repoid)
1128 raise JSONRPCError('failed to update repo `%s`' % repoid)
1128 raise JSONRPCError('failed to update repo `%s`' % repoid)
1129
1129
1130
1130
1131 @jsonrpc_method()
1131 @jsonrpc_method()
1132 def fork_repo(request, apiuser, repoid, fork_name,
1132 def fork_repo(request, apiuser, repoid, fork_name,
1133 owner=Optional(OAttr('apiuser')),
1133 owner=Optional(OAttr('apiuser')),
1134 description=Optional(''),
1134 description=Optional(''),
1135 private=Optional(False),
1135 private=Optional(False),
1136 clone_uri=Optional(None),
1136 clone_uri=Optional(None),
1137 landing_rev=Optional(None),
1137 landing_rev=Optional(None),
1138 copy_permissions=Optional(False)):
1138 copy_permissions=Optional(False)):
1139 """
1139 """
1140 Creates a fork of the specified |repo|.
1140 Creates a fork of the specified |repo|.
1141
1141
1142 * If the fork_name contains "/", fork will be created inside
1142 * If the fork_name contains "/", fork will be created inside
1143 a repository group or nested repository groups
1143 a repository group or nested repository groups
1144
1144
1145 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1145 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1146 inside group "foo/bar". You have to have permissions to access and
1146 inside group "foo/bar". You have to have permissions to access and
1147 write to the last repository group ("bar" in this example)
1147 write to the last repository group ("bar" in this example)
1148
1148
1149 This command can only be run using an |authtoken| with minimum
1149 This command can only be run using an |authtoken| with minimum
1150 read permissions of the forked repo, create fork permissions for an user.
1150 read permissions of the forked repo, create fork permissions for an user.
1151
1151
1152 :param apiuser: This is filled automatically from the |authtoken|.
1152 :param apiuser: This is filled automatically from the |authtoken|.
1153 :type apiuser: AuthUser
1153 :type apiuser: AuthUser
1154 :param repoid: Set repository name or repository ID.
1154 :param repoid: Set repository name or repository ID.
1155 :type repoid: str or int
1155 :type repoid: str or int
1156 :param fork_name: Set the fork name, including it's repository group membership.
1156 :param fork_name: Set the fork name, including it's repository group membership.
1157 :type fork_name: str
1157 :type fork_name: str
1158 :param owner: Set the fork owner.
1158 :param owner: Set the fork owner.
1159 :type owner: str
1159 :type owner: str
1160 :param description: Set the fork description.
1160 :param description: Set the fork description.
1161 :type description: str
1161 :type description: str
1162 :param copy_permissions: Copy permissions from parent |repo|. The
1162 :param copy_permissions: Copy permissions from parent |repo|. The
1163 default is False.
1163 default is False.
1164 :type copy_permissions: bool
1164 :type copy_permissions: bool
1165 :param private: Make the fork private. The default is False.
1165 :param private: Make the fork private. The default is False.
1166 :type private: bool
1166 :type private: bool
1167 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1167 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1168
1168
1169 Example output:
1169 Example output:
1170
1170
1171 .. code-block:: bash
1171 .. code-block:: bash
1172
1172
1173 id : <id_for_response>
1173 id : <id_for_response>
1174 api_key : "<api_key>"
1174 api_key : "<api_key>"
1175 args: {
1175 args: {
1176 "repoid" : "<reponame or repo_id>",
1176 "repoid" : "<reponame or repo_id>",
1177 "fork_name": "<forkname>",
1177 "fork_name": "<forkname>",
1178 "owner": "<username or user_id = Optional(=apiuser)>",
1178 "owner": "<username or user_id = Optional(=apiuser)>",
1179 "description": "<description>",
1179 "description": "<description>",
1180 "copy_permissions": "<bool>",
1180 "copy_permissions": "<bool>",
1181 "private": "<bool>",
1181 "private": "<bool>",
1182 "landing_rev": "<landing_rev>"
1182 "landing_rev": "<landing_rev>"
1183 }
1183 }
1184
1184
1185 Example error output:
1185 Example error output:
1186
1186
1187 .. code-block:: bash
1187 .. code-block:: bash
1188
1188
1189 id : <id_given_in_input>
1189 id : <id_given_in_input>
1190 result: {
1190 result: {
1191 "msg": "Created fork of `<reponame>` as `<forkname>`",
1191 "msg": "Created fork of `<reponame>` as `<forkname>`",
1192 "success": true,
1192 "success": true,
1193 "task": "<celery task id or None if done sync>"
1193 "task": "<celery task id or None if done sync>"
1194 }
1194 }
1195 error: null
1195 error: null
1196
1196
1197 """
1197 """
1198
1198
1199 repo = get_repo_or_error(repoid)
1199 repo = get_repo_or_error(repoid)
1200 repo_name = repo.repo_name
1200 repo_name = repo.repo_name
1201
1201
1202 if not has_superadmin_permission(apiuser):
1202 if not has_superadmin_permission(apiuser):
1203 # check if we have at least read permission for
1203 # check if we have at least read permission for
1204 # this repo that we fork !
1204 # this repo that we fork !
1205 _perms = (
1205 _perms = (
1206 'repository.admin', 'repository.write', 'repository.read')
1206 'repository.admin', 'repository.write', 'repository.read')
1207 validate_repo_permissions(apiuser, repoid, repo, _perms)
1207 validate_repo_permissions(apiuser, repoid, repo, _perms)
1208
1208
1209 # check if the regular user has at least fork permissions as well
1209 # check if the regular user has at least fork permissions as well
1210 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1210 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1211 raise JSONRPCForbidden()
1211 raise JSONRPCForbidden()
1212
1212
1213 # check if user can set owner parameter
1213 # check if user can set owner parameter
1214 owner = validate_set_owner_permissions(apiuser, owner)
1214 owner = validate_set_owner_permissions(apiuser, owner)
1215
1215
1216 description = Optional.extract(description)
1216 description = Optional.extract(description)
1217 copy_permissions = Optional.extract(copy_permissions)
1217 copy_permissions = Optional.extract(copy_permissions)
1218 clone_uri = Optional.extract(clone_uri)
1218 clone_uri = Optional.extract(clone_uri)
1219
1219
1220 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1220 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1221 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1221 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1222 ref_choices = list(set(ref_choices + [landing_ref]))
1222 ref_choices = list(set(ref_choices + [landing_ref]))
1223 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1223 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1224
1224
1225 private = Optional.extract(private)
1225 private = Optional.extract(private)
1226
1226
1227 schema = repo_schema.RepoSchema().bind(
1227 schema = repo_schema.RepoSchema().bind(
1228 repo_type_options=rhodecode.BACKENDS.keys(),
1228 repo_type_options=rhodecode.BACKENDS.keys(),
1229 repo_ref_options=ref_choices,
1229 repo_ref_options=ref_choices,
1230 repo_type=repo.repo_type,
1230 repo_type=repo.repo_type,
1231 # user caller
1231 # user caller
1232 user=apiuser)
1232 user=apiuser)
1233
1233
1234 try:
1234 try:
1235 schema_data = schema.deserialize(dict(
1235 schema_data = schema.deserialize(dict(
1236 repo_name=fork_name,
1236 repo_name=fork_name,
1237 repo_type=repo.repo_type,
1237 repo_type=repo.repo_type,
1238 repo_owner=owner.username,
1238 repo_owner=owner.username,
1239 repo_description=description,
1239 repo_description=description,
1240 repo_landing_commit_ref=landing_commit_ref,
1240 repo_landing_commit_ref=landing_commit_ref,
1241 repo_clone_uri=clone_uri,
1241 repo_clone_uri=clone_uri,
1242 repo_private=private,
1242 repo_private=private,
1243 repo_copy_permissions=copy_permissions))
1243 repo_copy_permissions=copy_permissions))
1244 except validation_schema.Invalid as err:
1244 except validation_schema.Invalid as err:
1245 raise JSONRPCValidationError(colander_exc=err)
1245 raise JSONRPCValidationError(colander_exc=err)
1246
1246
1247 try:
1247 try:
1248 data = {
1248 data = {
1249 'fork_parent_id': repo.repo_id,
1249 'fork_parent_id': repo.repo_id,
1250
1250
1251 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1251 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1252 'repo_name_full': schema_data['repo_name'],
1252 'repo_name_full': schema_data['repo_name'],
1253 'repo_group': schema_data['repo_group']['repo_group_id'],
1253 'repo_group': schema_data['repo_group']['repo_group_id'],
1254 'repo_type': schema_data['repo_type'],
1254 'repo_type': schema_data['repo_type'],
1255 'description': schema_data['repo_description'],
1255 'description': schema_data['repo_description'],
1256 'private': schema_data['repo_private'],
1256 'private': schema_data['repo_private'],
1257 'copy_permissions': schema_data['repo_copy_permissions'],
1257 'copy_permissions': schema_data['repo_copy_permissions'],
1258 'landing_rev': schema_data['repo_landing_commit_ref'],
1258 'landing_rev': schema_data['repo_landing_commit_ref'],
1259 }
1259 }
1260
1260
1261 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1261 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1262 # no commit, it's done in RepoModel, or async via celery
1262 # no commit, it's done in RepoModel, or async via celery
1263 task_id = get_task_id(task)
1263 task_id = get_task_id(task)
1264
1264
1265 return {
1265 return {
1266 'msg': 'Created fork of `%s` as `%s`' % (
1266 'msg': 'Created fork of `%s` as `%s`' % (
1267 repo.repo_name, schema_data['repo_name']),
1267 repo.repo_name, schema_data['repo_name']),
1268 'success': True, # cannot return the repo data here since fork
1268 'success': True, # cannot return the repo data here since fork
1269 # can be done async
1269 # can be done async
1270 'task': task_id
1270 'task': task_id
1271 }
1271 }
1272 except Exception:
1272 except Exception:
1273 log.exception(
1273 log.exception(
1274 u"Exception while trying to create fork %s",
1274 u"Exception while trying to create fork %s",
1275 schema_data['repo_name'])
1275 schema_data['repo_name'])
1276 raise JSONRPCError(
1276 raise JSONRPCError(
1277 'failed to fork repository `%s` as `%s`' % (
1277 'failed to fork repository `%s` as `%s`' % (
1278 repo_name, schema_data['repo_name']))
1278 repo_name, schema_data['repo_name']))
1279
1279
1280
1280
1281 @jsonrpc_method()
1281 @jsonrpc_method()
1282 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1282 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1283 """
1283 """
1284 Deletes a repository.
1284 Deletes a repository.
1285
1285
1286 * When the `forks` parameter is set it's possible to detach or delete
1286 * When the `forks` parameter is set it's possible to detach or delete
1287 forks of deleted repository.
1287 forks of deleted repository.
1288
1288
1289 This command can only be run using an |authtoken| with admin
1289 This command can only be run using an |authtoken| with admin
1290 permissions on the |repo|.
1290 permissions on the |repo|.
1291
1291
1292 :param apiuser: This is filled automatically from the |authtoken|.
1292 :param apiuser: This is filled automatically from the |authtoken|.
1293 :type apiuser: AuthUser
1293 :type apiuser: AuthUser
1294 :param repoid: Set the repository name or repository ID.
1294 :param repoid: Set the repository name or repository ID.
1295 :type repoid: str or int
1295 :type repoid: str or int
1296 :param forks: Set to `detach` or `delete` forks from the |repo|.
1296 :param forks: Set to `detach` or `delete` forks from the |repo|.
1297 :type forks: Optional(str)
1297 :type forks: Optional(str)
1298
1298
1299 Example error output:
1299 Example error output:
1300
1300
1301 .. code-block:: bash
1301 .. code-block:: bash
1302
1302
1303 id : <id_given_in_input>
1303 id : <id_given_in_input>
1304 result: {
1304 result: {
1305 "msg": "Deleted repository `<reponame>`",
1305 "msg": "Deleted repository `<reponame>`",
1306 "success": true
1306 "success": true
1307 }
1307 }
1308 error: null
1308 error: null
1309 """
1309 """
1310
1310
1311 repo = get_repo_or_error(repoid)
1311 repo = get_repo_or_error(repoid)
1312 repo_name = repo.repo_name
1312 repo_name = repo.repo_name
1313 if not has_superadmin_permission(apiuser):
1313 if not has_superadmin_permission(apiuser):
1314 _perms = ('repository.admin',)
1314 _perms = ('repository.admin',)
1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1316
1316
1317 try:
1317 try:
1318 handle_forks = Optional.extract(forks)
1318 handle_forks = Optional.extract(forks)
1319 _forks_msg = ''
1319 _forks_msg = ''
1320 _forks = [f for f in repo.forks]
1320 _forks = [f for f in repo.forks]
1321 if handle_forks == 'detach':
1321 if handle_forks == 'detach':
1322 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1322 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1323 elif handle_forks == 'delete':
1323 elif handle_forks == 'delete':
1324 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1324 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1325 elif _forks:
1325 elif _forks:
1326 raise JSONRPCError(
1326 raise JSONRPCError(
1327 'Cannot delete `%s` it still contains attached forks' %
1327 'Cannot delete `%s` it still contains attached forks' %
1328 (repo.repo_name,)
1328 (repo.repo_name,)
1329 )
1329 )
1330 old_data = repo.get_api_data()
1330 old_data = repo.get_api_data()
1331 RepoModel().delete(repo, forks=forks)
1331 RepoModel().delete(repo, forks=forks)
1332
1332
1333 repo = audit_logger.RepoWrap(repo_id=None,
1333 repo = audit_logger.RepoWrap(repo_id=None,
1334 repo_name=repo.repo_name)
1334 repo_name=repo.repo_name)
1335
1335
1336 audit_logger.store_api(
1336 audit_logger.store_api(
1337 'repo.delete', action_data={'old_data': old_data},
1337 'repo.delete', action_data={'old_data': old_data},
1338 user=apiuser, repo=repo)
1338 user=apiuser, repo=repo)
1339
1339
1340 ScmModel().mark_for_invalidation(repo_name, delete=True)
1340 ScmModel().mark_for_invalidation(repo_name, delete=True)
1341 Session().commit()
1341 Session().commit()
1342 return {
1342 return {
1343 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1343 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1344 'success': True
1344 'success': True
1345 }
1345 }
1346 except Exception:
1346 except Exception:
1347 log.exception("Exception occurred while trying to delete repo")
1347 log.exception("Exception occurred while trying to delete repo")
1348 raise JSONRPCError(
1348 raise JSONRPCError(
1349 'failed to delete repository `%s`' % (repo_name,)
1349 'failed to delete repository `%s`' % (repo_name,)
1350 )
1350 )
1351
1351
1352
1352
1353 #TODO: marcink, change name ?
1353 #TODO: marcink, change name ?
1354 @jsonrpc_method()
1354 @jsonrpc_method()
1355 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1355 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1356 """
1356 """
1357 Invalidates the cache for the specified repository.
1357 Invalidates the cache for the specified repository.
1358
1358
1359 This command can only be run using an |authtoken| with admin rights to
1359 This command can only be run using an |authtoken| with admin rights to
1360 the specified repository.
1360 the specified repository.
1361
1361
1362 This command takes the following options:
1362 This command takes the following options:
1363
1363
1364 :param apiuser: This is filled automatically from |authtoken|.
1364 :param apiuser: This is filled automatically from |authtoken|.
1365 :type apiuser: AuthUser
1365 :type apiuser: AuthUser
1366 :param repoid: Sets the repository name or repository ID.
1366 :param repoid: Sets the repository name or repository ID.
1367 :type repoid: str or int
1367 :type repoid: str or int
1368 :param delete_keys: This deletes the invalidated keys instead of
1368 :param delete_keys: This deletes the invalidated keys instead of
1369 just flagging them.
1369 just flagging them.
1370 :type delete_keys: Optional(``True`` | ``False``)
1370 :type delete_keys: Optional(``True`` | ``False``)
1371
1371
1372 Example output:
1372 Example output:
1373
1373
1374 .. code-block:: bash
1374 .. code-block:: bash
1375
1375
1376 id : <id_given_in_input>
1376 id : <id_given_in_input>
1377 result : {
1377 result : {
1378 'msg': Cache for repository `<repository name>` was invalidated,
1378 'msg': Cache for repository `<repository name>` was invalidated,
1379 'repository': <repository name>
1379 'repository': <repository name>
1380 }
1380 }
1381 error : null
1381 error : null
1382
1382
1383 Example error output:
1383 Example error output:
1384
1384
1385 .. code-block:: bash
1385 .. code-block:: bash
1386
1386
1387 id : <id_given_in_input>
1387 id : <id_given_in_input>
1388 result : null
1388 result : null
1389 error : {
1389 error : {
1390 'Error occurred during cache invalidation action'
1390 'Error occurred during cache invalidation action'
1391 }
1391 }
1392
1392
1393 """
1393 """
1394
1394
1395 repo = get_repo_or_error(repoid)
1395 repo = get_repo_or_error(repoid)
1396 if not has_superadmin_permission(apiuser):
1396 if not has_superadmin_permission(apiuser):
1397 _perms = ('repository.admin', 'repository.write',)
1397 _perms = ('repository.admin', 'repository.write',)
1398 validate_repo_permissions(apiuser, repoid, repo, _perms)
1398 validate_repo_permissions(apiuser, repoid, repo, _perms)
1399
1399
1400 delete = Optional.extract(delete_keys)
1400 delete = Optional.extract(delete_keys)
1401 try:
1401 try:
1402 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1402 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1403 return {
1403 return {
1404 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1404 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1405 'repository': repo.repo_name
1405 'repository': repo.repo_name
1406 }
1406 }
1407 except Exception:
1407 except Exception:
1408 log.exception(
1408 log.exception(
1409 "Exception occurred while trying to invalidate repo cache")
1409 "Exception occurred while trying to invalidate repo cache")
1410 raise JSONRPCError(
1410 raise JSONRPCError(
1411 'Error occurred during cache invalidation action'
1411 'Error occurred during cache invalidation action'
1412 )
1412 )
1413
1413
1414
1414
1415 #TODO: marcink, change name ?
1415 #TODO: marcink, change name ?
1416 @jsonrpc_method()
1416 @jsonrpc_method()
1417 def lock(request, apiuser, repoid, locked=Optional(None),
1417 def lock(request, apiuser, repoid, locked=Optional(None),
1418 userid=Optional(OAttr('apiuser'))):
1418 userid=Optional(OAttr('apiuser'))):
1419 """
1419 """
1420 Sets the lock state of the specified |repo| by the given user.
1420 Sets the lock state of the specified |repo| by the given user.
1421 From more information, see :ref:`repo-locking`.
1421 From more information, see :ref:`repo-locking`.
1422
1422
1423 * If the ``userid`` option is not set, the repository is locked to the
1423 * If the ``userid`` option is not set, the repository is locked to the
1424 user who called the method.
1424 user who called the method.
1425 * If the ``locked`` parameter is not set, the current lock state of the
1425 * If the ``locked`` parameter is not set, the current lock state of the
1426 repository is displayed.
1426 repository is displayed.
1427
1427
1428 This command can only be run using an |authtoken| with admin rights to
1428 This command can only be run using an |authtoken| with admin rights to
1429 the specified repository.
1429 the specified repository.
1430
1430
1431 This command takes the following options:
1431 This command takes the following options:
1432
1432
1433 :param apiuser: This is filled automatically from the |authtoken|.
1433 :param apiuser: This is filled automatically from the |authtoken|.
1434 :type apiuser: AuthUser
1434 :type apiuser: AuthUser
1435 :param repoid: Sets the repository name or repository ID.
1435 :param repoid: Sets the repository name or repository ID.
1436 :type repoid: str or int
1436 :type repoid: str or int
1437 :param locked: Sets the lock state.
1437 :param locked: Sets the lock state.
1438 :type locked: Optional(``True`` | ``False``)
1438 :type locked: Optional(``True`` | ``False``)
1439 :param userid: Set the repository lock to this user.
1439 :param userid: Set the repository lock to this user.
1440 :type userid: Optional(str or int)
1440 :type userid: Optional(str or int)
1441
1441
1442 Example error output:
1442 Example error output:
1443
1443
1444 .. code-block:: bash
1444 .. code-block:: bash
1445
1445
1446 id : <id_given_in_input>
1446 id : <id_given_in_input>
1447 result : {
1447 result : {
1448 'repo': '<reponame>',
1448 'repo': '<reponame>',
1449 'locked': <bool: lock state>,
1449 'locked': <bool: lock state>,
1450 'locked_since': <int: lock timestamp>,
1450 'locked_since': <int: lock timestamp>,
1451 'locked_by': <username of person who made the lock>,
1451 'locked_by': <username of person who made the lock>,
1452 'lock_reason': <str: reason for locking>,
1452 'lock_reason': <str: reason for locking>,
1453 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1453 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1454 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1454 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1455 or
1455 or
1456 'msg': 'Repo `<repository name>` not locked.'
1456 'msg': 'Repo `<repository name>` not locked.'
1457 or
1457 or
1458 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1458 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1459 }
1459 }
1460 error : null
1460 error : null
1461
1461
1462 Example error output:
1462 Example error output:
1463
1463
1464 .. code-block:: bash
1464 .. code-block:: bash
1465
1465
1466 id : <id_given_in_input>
1466 id : <id_given_in_input>
1467 result : null
1467 result : null
1468 error : {
1468 error : {
1469 'Error occurred locking repository `<reponame>`'
1469 'Error occurred locking repository `<reponame>`'
1470 }
1470 }
1471 """
1471 """
1472
1472
1473 repo = get_repo_or_error(repoid)
1473 repo = get_repo_or_error(repoid)
1474 if not has_superadmin_permission(apiuser):
1474 if not has_superadmin_permission(apiuser):
1475 # check if we have at least write permission for this repo !
1475 # check if we have at least write permission for this repo !
1476 _perms = ('repository.admin', 'repository.write',)
1476 _perms = ('repository.admin', 'repository.write',)
1477 validate_repo_permissions(apiuser, repoid, repo, _perms)
1477 validate_repo_permissions(apiuser, repoid, repo, _perms)
1478
1478
1479 # make sure normal user does not pass someone else userid,
1479 # make sure normal user does not pass someone else userid,
1480 # he is not allowed to do that
1480 # he is not allowed to do that
1481 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1481 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1482 raise JSONRPCError('userid is not the same as your user')
1482 raise JSONRPCError('userid is not the same as your user')
1483
1483
1484 if isinstance(userid, Optional):
1484 if isinstance(userid, Optional):
1485 userid = apiuser.user_id
1485 userid = apiuser.user_id
1486
1486
1487 user = get_user_or_error(userid)
1487 user = get_user_or_error(userid)
1488
1488
1489 if isinstance(locked, Optional):
1489 if isinstance(locked, Optional):
1490 lockobj = repo.locked
1490 lockobj = repo.locked
1491
1491
1492 if lockobj[0] is None:
1492 if lockobj[0] is None:
1493 _d = {
1493 _d = {
1494 'repo': repo.repo_name,
1494 'repo': repo.repo_name,
1495 'locked': False,
1495 'locked': False,
1496 'locked_since': None,
1496 'locked_since': None,
1497 'locked_by': None,
1497 'locked_by': None,
1498 'lock_reason': None,
1498 'lock_reason': None,
1499 'lock_state_changed': False,
1499 'lock_state_changed': False,
1500 'msg': 'Repo `%s` not locked.' % repo.repo_name
1500 'msg': 'Repo `%s` not locked.' % repo.repo_name
1501 }
1501 }
1502 return _d
1502 return _d
1503 else:
1503 else:
1504 _user_id, _time, _reason = lockobj
1504 _user_id, _time, _reason = lockobj
1505 lock_user = get_user_or_error(userid)
1505 lock_user = get_user_or_error(userid)
1506 _d = {
1506 _d = {
1507 'repo': repo.repo_name,
1507 'repo': repo.repo_name,
1508 'locked': True,
1508 'locked': True,
1509 'locked_since': _time,
1509 'locked_since': _time,
1510 'locked_by': lock_user.username,
1510 'locked_by': lock_user.username,
1511 'lock_reason': _reason,
1511 'lock_reason': _reason,
1512 'lock_state_changed': False,
1512 'lock_state_changed': False,
1513 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1513 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1514 % (repo.repo_name, lock_user.username,
1514 % (repo.repo_name, lock_user.username,
1515 json.dumps(time_to_datetime(_time))))
1515 json.dumps(time_to_datetime(_time))))
1516 }
1516 }
1517 return _d
1517 return _d
1518
1518
1519 # force locked state through a flag
1519 # force locked state through a flag
1520 else:
1520 else:
1521 locked = str2bool(locked)
1521 locked = str2bool(locked)
1522 lock_reason = Repository.LOCK_API
1522 lock_reason = Repository.LOCK_API
1523 try:
1523 try:
1524 if locked:
1524 if locked:
1525 lock_time = time.time()
1525 lock_time = time.time()
1526 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1526 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1527 else:
1527 else:
1528 lock_time = None
1528 lock_time = None
1529 Repository.unlock(repo)
1529 Repository.unlock(repo)
1530 _d = {
1530 _d = {
1531 'repo': repo.repo_name,
1531 'repo': repo.repo_name,
1532 'locked': locked,
1532 'locked': locked,
1533 'locked_since': lock_time,
1533 'locked_since': lock_time,
1534 'locked_by': user.username,
1534 'locked_by': user.username,
1535 'lock_reason': lock_reason,
1535 'lock_reason': lock_reason,
1536 'lock_state_changed': True,
1536 'lock_state_changed': True,
1537 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1537 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1538 % (user.username, repo.repo_name, locked))
1538 % (user.username, repo.repo_name, locked))
1539 }
1539 }
1540 return _d
1540 return _d
1541 except Exception:
1541 except Exception:
1542 log.exception(
1542 log.exception(
1543 "Exception occurred while trying to lock repository")
1543 "Exception occurred while trying to lock repository")
1544 raise JSONRPCError(
1544 raise JSONRPCError(
1545 'Error occurred locking repository `%s`' % repo.repo_name
1545 'Error occurred locking repository `%s`' % repo.repo_name
1546 )
1546 )
1547
1547
1548
1548
1549 @jsonrpc_method()
1549 @jsonrpc_method()
1550 def comment_commit(
1550 def comment_commit(
1551 request, apiuser, repoid, commit_id, message, status=Optional(None),
1551 request, apiuser, repoid, commit_id, message, status=Optional(None),
1552 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1552 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1553 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1553 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1554 userid=Optional(OAttr('apiuser'))):
1554 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1555 """
1555 """
1556 Set a commit comment, and optionally change the status of the commit.
1556 Set a commit comment, and optionally change the status of the commit.
1557
1557
1558 :param apiuser: This is filled automatically from the |authtoken|.
1558 :param apiuser: This is filled automatically from the |authtoken|.
1559 :type apiuser: AuthUser
1559 :type apiuser: AuthUser
1560 :param repoid: Set the repository name or repository ID.
1560 :param repoid: Set the repository name or repository ID.
1561 :type repoid: str or int
1561 :type repoid: str or int
1562 :param commit_id: Specify the commit_id for which to set a comment.
1562 :param commit_id: Specify the commit_id for which to set a comment.
1563 :type commit_id: str
1563 :type commit_id: str
1564 :param message: The comment text.
1564 :param message: The comment text.
1565 :type message: str
1565 :type message: str
1566 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1566 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1567 'approved', 'rejected', 'under_review'
1567 'approved', 'rejected', 'under_review'
1568 :type status: str
1568 :type status: str
1569 :param comment_type: Comment type, one of: 'note', 'todo'
1569 :param comment_type: Comment type, one of: 'note', 'todo'
1570 :type comment_type: Optional(str), default: 'note'
1570 :type comment_type: Optional(str), default: 'note'
1571 :param resolves_comment_id: id of comment which this one will resolve
1571 :param resolves_comment_id: id of comment which this one will resolve
1572 :type resolves_comment_id: Optional(int)
1572 :type resolves_comment_id: Optional(int)
1573 :param extra_recipients: list of user ids or usernames to add
1573 :param extra_recipients: list of user ids or usernames to add
1574 notifications for this comment. Acts like a CC for notification
1574 notifications for this comment. Acts like a CC for notification
1575 :type extra_recipients: Optional(list)
1575 :type extra_recipients: Optional(list)
1576 :param userid: Set the user name of the comment creator.
1576 :param userid: Set the user name of the comment creator.
1577 :type userid: Optional(str or int)
1577 :type userid: Optional(str or int)
1578 :param send_email: Define if this comment should also send email notification
1579 :type send_email: Optional(bool)
1578
1580
1579 Example error output:
1581 Example error output:
1580
1582
1581 .. code-block:: bash
1583 .. code-block:: bash
1582
1584
1583 {
1585 {
1584 "id" : <id_given_in_input>,
1586 "id" : <id_given_in_input>,
1585 "result" : {
1587 "result" : {
1586 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1588 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1587 "status_change": null or <status>,
1589 "status_change": null or <status>,
1588 "success": true
1590 "success": true
1589 },
1591 },
1590 "error" : null
1592 "error" : null
1591 }
1593 }
1592
1594
1593 """
1595 """
1594 repo = get_repo_or_error(repoid)
1596 repo = get_repo_or_error(repoid)
1595 if not has_superadmin_permission(apiuser):
1597 if not has_superadmin_permission(apiuser):
1596 _perms = ('repository.read', 'repository.write', 'repository.admin')
1598 _perms = ('repository.read', 'repository.write', 'repository.admin')
1597 validate_repo_permissions(apiuser, repoid, repo, _perms)
1599 validate_repo_permissions(apiuser, repoid, repo, _perms)
1598
1600
1599 try:
1601 try:
1600 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1602 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1601 except Exception as e:
1603 except Exception as e:
1602 log.exception('Failed to fetch commit')
1604 log.exception('Failed to fetch commit')
1603 raise JSONRPCError(safe_str(e))
1605 raise JSONRPCError(safe_str(e))
1604
1606
1605 if isinstance(userid, Optional):
1607 if isinstance(userid, Optional):
1606 userid = apiuser.user_id
1608 userid = apiuser.user_id
1607
1609
1608 user = get_user_or_error(userid)
1610 user = get_user_or_error(userid)
1609 status = Optional.extract(status)
1611 status = Optional.extract(status)
1610 comment_type = Optional.extract(comment_type)
1612 comment_type = Optional.extract(comment_type)
1611 resolves_comment_id = Optional.extract(resolves_comment_id)
1613 resolves_comment_id = Optional.extract(resolves_comment_id)
1612 extra_recipients = Optional.extract(extra_recipients)
1614 extra_recipients = Optional.extract(extra_recipients)
1615 send_email = Optional.extract(send_email, binary=True)
1613
1616
1614 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1617 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1615 if status and status not in allowed_statuses:
1618 if status and status not in allowed_statuses:
1616 raise JSONRPCError('Bad status, must be on '
1619 raise JSONRPCError('Bad status, must be on '
1617 'of %s got %s' % (allowed_statuses, status,))
1620 'of %s got %s' % (allowed_statuses, status,))
1618
1621
1619 if resolves_comment_id:
1622 if resolves_comment_id:
1620 comment = ChangesetComment.get(resolves_comment_id)
1623 comment = ChangesetComment.get(resolves_comment_id)
1621 if not comment:
1624 if not comment:
1622 raise JSONRPCError(
1625 raise JSONRPCError(
1623 'Invalid resolves_comment_id `%s` for this commit.'
1626 'Invalid resolves_comment_id `%s` for this commit.'
1624 % resolves_comment_id)
1627 % resolves_comment_id)
1625 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1628 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1626 raise JSONRPCError(
1629 raise JSONRPCError(
1627 'Comment `%s` is wrong type for setting status to resolved.'
1630 'Comment `%s` is wrong type for setting status to resolved.'
1628 % resolves_comment_id)
1631 % resolves_comment_id)
1629
1632
1630 try:
1633 try:
1631 rc_config = SettingsModel().get_all_settings()
1634 rc_config = SettingsModel().get_all_settings()
1632 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1635 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1633 status_change_label = ChangesetStatus.get_status_lbl(status)
1636 status_change_label = ChangesetStatus.get_status_lbl(status)
1634 comment = CommentsModel().create(
1637 comment = CommentsModel().create(
1635 message, repo, user, commit_id=commit_id,
1638 message, repo, user, commit_id=commit_id,
1636 status_change=status_change_label,
1639 status_change=status_change_label,
1637 status_change_type=status,
1640 status_change_type=status,
1638 renderer=renderer,
1641 renderer=renderer,
1639 comment_type=comment_type,
1642 comment_type=comment_type,
1640 resolves_comment_id=resolves_comment_id,
1643 resolves_comment_id=resolves_comment_id,
1641 auth_user=apiuser,
1644 auth_user=apiuser,
1642 extra_recipients=extra_recipients
1645 extra_recipients=extra_recipients,
1646 send_email=send_email
1643 )
1647 )
1644 if status:
1648 if status:
1645 # also do a status change
1649 # also do a status change
1646 try:
1650 try:
1647 ChangesetStatusModel().set_status(
1651 ChangesetStatusModel().set_status(
1648 repo, status, user, comment, revision=commit_id,
1652 repo, status, user, comment, revision=commit_id,
1649 dont_allow_on_closed_pull_request=True
1653 dont_allow_on_closed_pull_request=True
1650 )
1654 )
1651 except StatusChangeOnClosedPullRequestError:
1655 except StatusChangeOnClosedPullRequestError:
1652 log.exception(
1656 log.exception(
1653 "Exception occurred while trying to change repo commit status")
1657 "Exception occurred while trying to change repo commit status")
1654 msg = ('Changing status on a changeset associated with '
1658 msg = ('Changing status on a changeset associated with '
1655 'a closed pull request is not allowed')
1659 'a closed pull request is not allowed')
1656 raise JSONRPCError(msg)
1660 raise JSONRPCError(msg)
1657
1661
1658 Session().commit()
1662 Session().commit()
1659 return {
1663 return {
1660 'msg': (
1664 'msg': (
1661 'Commented on commit `%s` for repository `%s`' % (
1665 'Commented on commit `%s` for repository `%s`' % (
1662 comment.revision, repo.repo_name)),
1666 comment.revision, repo.repo_name)),
1663 'status_change': status,
1667 'status_change': status,
1664 'success': True,
1668 'success': True,
1665 }
1669 }
1666 except JSONRPCError:
1670 except JSONRPCError:
1667 # catch any inside errors, and re-raise them to prevent from
1671 # catch any inside errors, and re-raise them to prevent from
1668 # below global catch to silence them
1672 # below global catch to silence them
1669 raise
1673 raise
1670 except Exception:
1674 except Exception:
1671 log.exception("Exception occurred while trying to comment on commit")
1675 log.exception("Exception occurred while trying to comment on commit")
1672 raise JSONRPCError(
1676 raise JSONRPCError(
1673 'failed to set comment on repository `%s`' % (repo.repo_name,)
1677 'failed to set comment on repository `%s`' % (repo.repo_name,)
1674 )
1678 )
1675
1679
1676
1680
1677 @jsonrpc_method()
1681 @jsonrpc_method()
1678 def get_repo_comments(request, apiuser, repoid,
1682 def get_repo_comments(request, apiuser, repoid,
1679 commit_id=Optional(None), comment_type=Optional(None),
1683 commit_id=Optional(None), comment_type=Optional(None),
1680 userid=Optional(None)):
1684 userid=Optional(None)):
1681 """
1685 """
1682 Get all comments for a repository
1686 Get all comments for a repository
1683
1687
1684 :param apiuser: This is filled automatically from the |authtoken|.
1688 :param apiuser: This is filled automatically from the |authtoken|.
1685 :type apiuser: AuthUser
1689 :type apiuser: AuthUser
1686 :param repoid: Set the repository name or repository ID.
1690 :param repoid: Set the repository name or repository ID.
1687 :type repoid: str or int
1691 :type repoid: str or int
1688 :param commit_id: Optionally filter the comments by the commit_id
1692 :param commit_id: Optionally filter the comments by the commit_id
1689 :type commit_id: Optional(str), default: None
1693 :type commit_id: Optional(str), default: None
1690 :param comment_type: Optionally filter the comments by the comment_type
1694 :param comment_type: Optionally filter the comments by the comment_type
1691 one of: 'note', 'todo'
1695 one of: 'note', 'todo'
1692 :type comment_type: Optional(str), default: None
1696 :type comment_type: Optional(str), default: None
1693 :param userid: Optionally filter the comments by the author of comment
1697 :param userid: Optionally filter the comments by the author of comment
1694 :type userid: Optional(str or int), Default: None
1698 :type userid: Optional(str or int), Default: None
1695
1699
1696 Example error output:
1700 Example error output:
1697
1701
1698 .. code-block:: bash
1702 .. code-block:: bash
1699
1703
1700 {
1704 {
1701 "id" : <id_given_in_input>,
1705 "id" : <id_given_in_input>,
1702 "result" : [
1706 "result" : [
1703 {
1707 {
1704 "comment_author": <USER_DETAILS>,
1708 "comment_author": <USER_DETAILS>,
1705 "comment_created_on": "2017-02-01T14:38:16.309",
1709 "comment_created_on": "2017-02-01T14:38:16.309",
1706 "comment_f_path": "file.txt",
1710 "comment_f_path": "file.txt",
1707 "comment_id": 282,
1711 "comment_id": 282,
1708 "comment_lineno": "n1",
1712 "comment_lineno": "n1",
1709 "comment_resolved_by": null,
1713 "comment_resolved_by": null,
1710 "comment_status": [],
1714 "comment_status": [],
1711 "comment_text": "This file needs a header",
1715 "comment_text": "This file needs a header",
1712 "comment_type": "todo"
1716 "comment_type": "todo"
1713 }
1717 }
1714 ],
1718 ],
1715 "error" : null
1719 "error" : null
1716 }
1720 }
1717
1721
1718 """
1722 """
1719 repo = get_repo_or_error(repoid)
1723 repo = get_repo_or_error(repoid)
1720 if not has_superadmin_permission(apiuser):
1724 if not has_superadmin_permission(apiuser):
1721 _perms = ('repository.read', 'repository.write', 'repository.admin')
1725 _perms = ('repository.read', 'repository.write', 'repository.admin')
1722 validate_repo_permissions(apiuser, repoid, repo, _perms)
1726 validate_repo_permissions(apiuser, repoid, repo, _perms)
1723
1727
1724 commit_id = Optional.extract(commit_id)
1728 commit_id = Optional.extract(commit_id)
1725
1729
1726 userid = Optional.extract(userid)
1730 userid = Optional.extract(userid)
1727 if userid:
1731 if userid:
1728 user = get_user_or_error(userid)
1732 user = get_user_or_error(userid)
1729 else:
1733 else:
1730 user = None
1734 user = None
1731
1735
1732 comment_type = Optional.extract(comment_type)
1736 comment_type = Optional.extract(comment_type)
1733 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1737 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1734 raise JSONRPCError(
1738 raise JSONRPCError(
1735 'comment_type must be one of `{}` got {}'.format(
1739 'comment_type must be one of `{}` got {}'.format(
1736 ChangesetComment.COMMENT_TYPES, comment_type)
1740 ChangesetComment.COMMENT_TYPES, comment_type)
1737 )
1741 )
1738
1742
1739 comments = CommentsModel().get_repository_comments(
1743 comments = CommentsModel().get_repository_comments(
1740 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1744 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1741 return comments
1745 return comments
1742
1746
1743
1747
1744 @jsonrpc_method()
1748 @jsonrpc_method()
1745 def grant_user_permission(request, apiuser, repoid, userid, perm):
1749 def grant_user_permission(request, apiuser, repoid, userid, perm):
1746 """
1750 """
1747 Grant permissions for the specified user on the given repository,
1751 Grant permissions for the specified user on the given repository,
1748 or update existing permissions if found.
1752 or update existing permissions if found.
1749
1753
1750 This command can only be run using an |authtoken| with admin
1754 This command can only be run using an |authtoken| with admin
1751 permissions on the |repo|.
1755 permissions on the |repo|.
1752
1756
1753 :param apiuser: This is filled automatically from the |authtoken|.
1757 :param apiuser: This is filled automatically from the |authtoken|.
1754 :type apiuser: AuthUser
1758 :type apiuser: AuthUser
1755 :param repoid: Set the repository name or repository ID.
1759 :param repoid: Set the repository name or repository ID.
1756 :type repoid: str or int
1760 :type repoid: str or int
1757 :param userid: Set the user name.
1761 :param userid: Set the user name.
1758 :type userid: str
1762 :type userid: str
1759 :param perm: Set the user permissions, using the following format
1763 :param perm: Set the user permissions, using the following format
1760 ``(repository.(none|read|write|admin))``
1764 ``(repository.(none|read|write|admin))``
1761 :type perm: str
1765 :type perm: str
1762
1766
1763 Example output:
1767 Example output:
1764
1768
1765 .. code-block:: bash
1769 .. code-block:: bash
1766
1770
1767 id : <id_given_in_input>
1771 id : <id_given_in_input>
1768 result: {
1772 result: {
1769 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1773 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1770 "success": true
1774 "success": true
1771 }
1775 }
1772 error: null
1776 error: null
1773 """
1777 """
1774
1778
1775 repo = get_repo_or_error(repoid)
1779 repo = get_repo_or_error(repoid)
1776 user = get_user_or_error(userid)
1780 user = get_user_or_error(userid)
1777 perm = get_perm_or_error(perm)
1781 perm = get_perm_or_error(perm)
1778 if not has_superadmin_permission(apiuser):
1782 if not has_superadmin_permission(apiuser):
1779 _perms = ('repository.admin',)
1783 _perms = ('repository.admin',)
1780 validate_repo_permissions(apiuser, repoid, repo, _perms)
1784 validate_repo_permissions(apiuser, repoid, repo, _perms)
1781
1785
1782 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1786 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1783 try:
1787 try:
1784 changes = RepoModel().update_permissions(
1788 changes = RepoModel().update_permissions(
1785 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1789 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1786
1790
1787 action_data = {
1791 action_data = {
1788 'added': changes['added'],
1792 'added': changes['added'],
1789 'updated': changes['updated'],
1793 'updated': changes['updated'],
1790 'deleted': changes['deleted'],
1794 'deleted': changes['deleted'],
1791 }
1795 }
1792 audit_logger.store_api(
1796 audit_logger.store_api(
1793 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1797 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1794 Session().commit()
1798 Session().commit()
1795 PermissionModel().flush_user_permission_caches(changes)
1799 PermissionModel().flush_user_permission_caches(changes)
1796
1800
1797 return {
1801 return {
1798 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1802 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1799 perm.permission_name, user.username, repo.repo_name
1803 perm.permission_name, user.username, repo.repo_name
1800 ),
1804 ),
1801 'success': True
1805 'success': True
1802 }
1806 }
1803 except Exception:
1807 except Exception:
1804 log.exception("Exception occurred while trying edit permissions for repo")
1808 log.exception("Exception occurred while trying edit permissions for repo")
1805 raise JSONRPCError(
1809 raise JSONRPCError(
1806 'failed to edit permission for user: `%s` in repo: `%s`' % (
1810 'failed to edit permission for user: `%s` in repo: `%s`' % (
1807 userid, repoid
1811 userid, repoid
1808 )
1812 )
1809 )
1813 )
1810
1814
1811
1815
1812 @jsonrpc_method()
1816 @jsonrpc_method()
1813 def revoke_user_permission(request, apiuser, repoid, userid):
1817 def revoke_user_permission(request, apiuser, repoid, userid):
1814 """
1818 """
1815 Revoke permission for a user on the specified repository.
1819 Revoke permission for a user on the specified repository.
1816
1820
1817 This command can only be run using an |authtoken| with admin
1821 This command can only be run using an |authtoken| with admin
1818 permissions on the |repo|.
1822 permissions on the |repo|.
1819
1823
1820 :param apiuser: This is filled automatically from the |authtoken|.
1824 :param apiuser: This is filled automatically from the |authtoken|.
1821 :type apiuser: AuthUser
1825 :type apiuser: AuthUser
1822 :param repoid: Set the repository name or repository ID.
1826 :param repoid: Set the repository name or repository ID.
1823 :type repoid: str or int
1827 :type repoid: str or int
1824 :param userid: Set the user name of revoked user.
1828 :param userid: Set the user name of revoked user.
1825 :type userid: str or int
1829 :type userid: str or int
1826
1830
1827 Example error output:
1831 Example error output:
1828
1832
1829 .. code-block:: bash
1833 .. code-block:: bash
1830
1834
1831 id : <id_given_in_input>
1835 id : <id_given_in_input>
1832 result: {
1836 result: {
1833 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1837 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1834 "success": true
1838 "success": true
1835 }
1839 }
1836 error: null
1840 error: null
1837 """
1841 """
1838
1842
1839 repo = get_repo_or_error(repoid)
1843 repo = get_repo_or_error(repoid)
1840 user = get_user_or_error(userid)
1844 user = get_user_or_error(userid)
1841 if not has_superadmin_permission(apiuser):
1845 if not has_superadmin_permission(apiuser):
1842 _perms = ('repository.admin',)
1846 _perms = ('repository.admin',)
1843 validate_repo_permissions(apiuser, repoid, repo, _perms)
1847 validate_repo_permissions(apiuser, repoid, repo, _perms)
1844
1848
1845 perm_deletions = [[user.user_id, None, "user"]]
1849 perm_deletions = [[user.user_id, None, "user"]]
1846 try:
1850 try:
1847 changes = RepoModel().update_permissions(
1851 changes = RepoModel().update_permissions(
1848 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1852 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1849
1853
1850 action_data = {
1854 action_data = {
1851 'added': changes['added'],
1855 'added': changes['added'],
1852 'updated': changes['updated'],
1856 'updated': changes['updated'],
1853 'deleted': changes['deleted'],
1857 'deleted': changes['deleted'],
1854 }
1858 }
1855 audit_logger.store_api(
1859 audit_logger.store_api(
1856 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1860 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1857 Session().commit()
1861 Session().commit()
1858 PermissionModel().flush_user_permission_caches(changes)
1862 PermissionModel().flush_user_permission_caches(changes)
1859
1863
1860 return {
1864 return {
1861 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1865 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1862 user.username, repo.repo_name
1866 user.username, repo.repo_name
1863 ),
1867 ),
1864 'success': True
1868 'success': True
1865 }
1869 }
1866 except Exception:
1870 except Exception:
1867 log.exception("Exception occurred while trying revoke permissions to repo")
1871 log.exception("Exception occurred while trying revoke permissions to repo")
1868 raise JSONRPCError(
1872 raise JSONRPCError(
1869 'failed to edit permission for user: `%s` in repo: `%s`' % (
1873 'failed to edit permission for user: `%s` in repo: `%s`' % (
1870 userid, repoid
1874 userid, repoid
1871 )
1875 )
1872 )
1876 )
1873
1877
1874
1878
1875 @jsonrpc_method()
1879 @jsonrpc_method()
1876 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1880 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1877 """
1881 """
1878 Grant permission for a user group on the specified repository,
1882 Grant permission for a user group on the specified repository,
1879 or update existing permissions.
1883 or update existing permissions.
1880
1884
1881 This command can only be run using an |authtoken| with admin
1885 This command can only be run using an |authtoken| with admin
1882 permissions on the |repo|.
1886 permissions on the |repo|.
1883
1887
1884 :param apiuser: This is filled automatically from the |authtoken|.
1888 :param apiuser: This is filled automatically from the |authtoken|.
1885 :type apiuser: AuthUser
1889 :type apiuser: AuthUser
1886 :param repoid: Set the repository name or repository ID.
1890 :param repoid: Set the repository name or repository ID.
1887 :type repoid: str or int
1891 :type repoid: str or int
1888 :param usergroupid: Specify the ID of the user group.
1892 :param usergroupid: Specify the ID of the user group.
1889 :type usergroupid: str or int
1893 :type usergroupid: str or int
1890 :param perm: Set the user group permissions using the following
1894 :param perm: Set the user group permissions using the following
1891 format: (repository.(none|read|write|admin))
1895 format: (repository.(none|read|write|admin))
1892 :type perm: str
1896 :type perm: str
1893
1897
1894 Example output:
1898 Example output:
1895
1899
1896 .. code-block:: bash
1900 .. code-block:: bash
1897
1901
1898 id : <id_given_in_input>
1902 id : <id_given_in_input>
1899 result : {
1903 result : {
1900 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1904 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1901 "success": true
1905 "success": true
1902
1906
1903 }
1907 }
1904 error : null
1908 error : null
1905
1909
1906 Example error output:
1910 Example error output:
1907
1911
1908 .. code-block:: bash
1912 .. code-block:: bash
1909
1913
1910 id : <id_given_in_input>
1914 id : <id_given_in_input>
1911 result : null
1915 result : null
1912 error : {
1916 error : {
1913 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1917 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1914 }
1918 }
1915
1919
1916 """
1920 """
1917
1921
1918 repo = get_repo_or_error(repoid)
1922 repo = get_repo_or_error(repoid)
1919 perm = get_perm_or_error(perm)
1923 perm = get_perm_or_error(perm)
1920 if not has_superadmin_permission(apiuser):
1924 if not has_superadmin_permission(apiuser):
1921 _perms = ('repository.admin',)
1925 _perms = ('repository.admin',)
1922 validate_repo_permissions(apiuser, repoid, repo, _perms)
1926 validate_repo_permissions(apiuser, repoid, repo, _perms)
1923
1927
1924 user_group = get_user_group_or_error(usergroupid)
1928 user_group = get_user_group_or_error(usergroupid)
1925 if not has_superadmin_permission(apiuser):
1929 if not has_superadmin_permission(apiuser):
1926 # check if we have at least read permission for this user group !
1930 # check if we have at least read permission for this user group !
1927 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1931 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1928 if not HasUserGroupPermissionAnyApi(*_perms)(
1932 if not HasUserGroupPermissionAnyApi(*_perms)(
1929 user=apiuser, user_group_name=user_group.users_group_name):
1933 user=apiuser, user_group_name=user_group.users_group_name):
1930 raise JSONRPCError(
1934 raise JSONRPCError(
1931 'user group `%s` does not exist' % (usergroupid,))
1935 'user group `%s` does not exist' % (usergroupid,))
1932
1936
1933 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1937 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1934 try:
1938 try:
1935 changes = RepoModel().update_permissions(
1939 changes = RepoModel().update_permissions(
1936 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1940 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1937 action_data = {
1941 action_data = {
1938 'added': changes['added'],
1942 'added': changes['added'],
1939 'updated': changes['updated'],
1943 'updated': changes['updated'],
1940 'deleted': changes['deleted'],
1944 'deleted': changes['deleted'],
1941 }
1945 }
1942 audit_logger.store_api(
1946 audit_logger.store_api(
1943 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1947 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1944 Session().commit()
1948 Session().commit()
1945 PermissionModel().flush_user_permission_caches(changes)
1949 PermissionModel().flush_user_permission_caches(changes)
1946
1950
1947 return {
1951 return {
1948 'msg': 'Granted perm: `%s` for user group: `%s` in '
1952 'msg': 'Granted perm: `%s` for user group: `%s` in '
1949 'repo: `%s`' % (
1953 'repo: `%s`' % (
1950 perm.permission_name, user_group.users_group_name,
1954 perm.permission_name, user_group.users_group_name,
1951 repo.repo_name
1955 repo.repo_name
1952 ),
1956 ),
1953 'success': True
1957 'success': True
1954 }
1958 }
1955 except Exception:
1959 except Exception:
1956 log.exception(
1960 log.exception(
1957 "Exception occurred while trying change permission on repo")
1961 "Exception occurred while trying change permission on repo")
1958 raise JSONRPCError(
1962 raise JSONRPCError(
1959 'failed to edit permission for user group: `%s` in '
1963 'failed to edit permission for user group: `%s` in '
1960 'repo: `%s`' % (
1964 'repo: `%s`' % (
1961 usergroupid, repo.repo_name
1965 usergroupid, repo.repo_name
1962 )
1966 )
1963 )
1967 )
1964
1968
1965
1969
1966 @jsonrpc_method()
1970 @jsonrpc_method()
1967 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1971 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1968 """
1972 """
1969 Revoke the permissions of a user group on a given repository.
1973 Revoke the permissions of a user group on a given repository.
1970
1974
1971 This command can only be run using an |authtoken| with admin
1975 This command can only be run using an |authtoken| with admin
1972 permissions on the |repo|.
1976 permissions on the |repo|.
1973
1977
1974 :param apiuser: This is filled automatically from the |authtoken|.
1978 :param apiuser: This is filled automatically from the |authtoken|.
1975 :type apiuser: AuthUser
1979 :type apiuser: AuthUser
1976 :param repoid: Set the repository name or repository ID.
1980 :param repoid: Set the repository name or repository ID.
1977 :type repoid: str or int
1981 :type repoid: str or int
1978 :param usergroupid: Specify the user group ID.
1982 :param usergroupid: Specify the user group ID.
1979 :type usergroupid: str or int
1983 :type usergroupid: str or int
1980
1984
1981 Example output:
1985 Example output:
1982
1986
1983 .. code-block:: bash
1987 .. code-block:: bash
1984
1988
1985 id : <id_given_in_input>
1989 id : <id_given_in_input>
1986 result: {
1990 result: {
1987 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1991 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1988 "success": true
1992 "success": true
1989 }
1993 }
1990 error: null
1994 error: null
1991 """
1995 """
1992
1996
1993 repo = get_repo_or_error(repoid)
1997 repo = get_repo_or_error(repoid)
1994 if not has_superadmin_permission(apiuser):
1998 if not has_superadmin_permission(apiuser):
1995 _perms = ('repository.admin',)
1999 _perms = ('repository.admin',)
1996 validate_repo_permissions(apiuser, repoid, repo, _perms)
2000 validate_repo_permissions(apiuser, repoid, repo, _perms)
1997
2001
1998 user_group = get_user_group_or_error(usergroupid)
2002 user_group = get_user_group_or_error(usergroupid)
1999 if not has_superadmin_permission(apiuser):
2003 if not has_superadmin_permission(apiuser):
2000 # check if we have at least read permission for this user group !
2004 # check if we have at least read permission for this user group !
2001 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2005 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2002 if not HasUserGroupPermissionAnyApi(*_perms)(
2006 if not HasUserGroupPermissionAnyApi(*_perms)(
2003 user=apiuser, user_group_name=user_group.users_group_name):
2007 user=apiuser, user_group_name=user_group.users_group_name):
2004 raise JSONRPCError(
2008 raise JSONRPCError(
2005 'user group `%s` does not exist' % (usergroupid,))
2009 'user group `%s` does not exist' % (usergroupid,))
2006
2010
2007 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2011 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2008 try:
2012 try:
2009 changes = RepoModel().update_permissions(
2013 changes = RepoModel().update_permissions(
2010 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2014 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2011 action_data = {
2015 action_data = {
2012 'added': changes['added'],
2016 'added': changes['added'],
2013 'updated': changes['updated'],
2017 'updated': changes['updated'],
2014 'deleted': changes['deleted'],
2018 'deleted': changes['deleted'],
2015 }
2019 }
2016 audit_logger.store_api(
2020 audit_logger.store_api(
2017 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2021 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2018 Session().commit()
2022 Session().commit()
2019 PermissionModel().flush_user_permission_caches(changes)
2023 PermissionModel().flush_user_permission_caches(changes)
2020
2024
2021 return {
2025 return {
2022 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2026 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2023 user_group.users_group_name, repo.repo_name
2027 user_group.users_group_name, repo.repo_name
2024 ),
2028 ),
2025 'success': True
2029 'success': True
2026 }
2030 }
2027 except Exception:
2031 except Exception:
2028 log.exception("Exception occurred while trying revoke "
2032 log.exception("Exception occurred while trying revoke "
2029 "user group permission on repo")
2033 "user group permission on repo")
2030 raise JSONRPCError(
2034 raise JSONRPCError(
2031 'failed to edit permission for user group: `%s` in '
2035 'failed to edit permission for user group: `%s` in '
2032 'repo: `%s`' % (
2036 'repo: `%s`' % (
2033 user_group.users_group_name, repo.repo_name
2037 user_group.users_group_name, repo.repo_name
2034 )
2038 )
2035 )
2039 )
2036
2040
2037
2041
2038 @jsonrpc_method()
2042 @jsonrpc_method()
2039 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2043 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2040 """
2044 """
2041 Triggers a pull on the given repository from a remote location. You
2045 Triggers a pull on the given repository from a remote location. You
2042 can use this to keep remote repositories up-to-date.
2046 can use this to keep remote repositories up-to-date.
2043
2047
2044 This command can only be run using an |authtoken| with admin
2048 This command can only be run using an |authtoken| with admin
2045 rights to the specified repository. For more information,
2049 rights to the specified repository. For more information,
2046 see :ref:`config-token-ref`.
2050 see :ref:`config-token-ref`.
2047
2051
2048 This command takes the following options:
2052 This command takes the following options:
2049
2053
2050 :param apiuser: This is filled automatically from the |authtoken|.
2054 :param apiuser: This is filled automatically from the |authtoken|.
2051 :type apiuser: AuthUser
2055 :type apiuser: AuthUser
2052 :param repoid: The repository name or repository ID.
2056 :param repoid: The repository name or repository ID.
2053 :type repoid: str or int
2057 :type repoid: str or int
2054 :param remote_uri: Optional remote URI to pass in for pull
2058 :param remote_uri: Optional remote URI to pass in for pull
2055 :type remote_uri: str
2059 :type remote_uri: str
2056
2060
2057 Example output:
2061 Example output:
2058
2062
2059 .. code-block:: bash
2063 .. code-block:: bash
2060
2064
2061 id : <id_given_in_input>
2065 id : <id_given_in_input>
2062 result : {
2066 result : {
2063 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2067 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2064 "repository": "<repository name>"
2068 "repository": "<repository name>"
2065 }
2069 }
2066 error : null
2070 error : null
2067
2071
2068 Example error output:
2072 Example error output:
2069
2073
2070 .. code-block:: bash
2074 .. code-block:: bash
2071
2075
2072 id : <id_given_in_input>
2076 id : <id_given_in_input>
2073 result : null
2077 result : null
2074 error : {
2078 error : {
2075 "Unable to push changes from `<remote_url>`"
2079 "Unable to push changes from `<remote_url>`"
2076 }
2080 }
2077
2081
2078 """
2082 """
2079
2083
2080 repo = get_repo_or_error(repoid)
2084 repo = get_repo_or_error(repoid)
2081 remote_uri = Optional.extract(remote_uri)
2085 remote_uri = Optional.extract(remote_uri)
2082 remote_uri_display = remote_uri or repo.clone_uri_hidden
2086 remote_uri_display = remote_uri or repo.clone_uri_hidden
2083 if not has_superadmin_permission(apiuser):
2087 if not has_superadmin_permission(apiuser):
2084 _perms = ('repository.admin',)
2088 _perms = ('repository.admin',)
2085 validate_repo_permissions(apiuser, repoid, repo, _perms)
2089 validate_repo_permissions(apiuser, repoid, repo, _perms)
2086
2090
2087 try:
2091 try:
2088 ScmModel().pull_changes(
2092 ScmModel().pull_changes(
2089 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2093 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2090 return {
2094 return {
2091 'msg': 'Pulled from url `%s` on repo `%s`' % (
2095 'msg': 'Pulled from url `%s` on repo `%s`' % (
2092 remote_uri_display, repo.repo_name),
2096 remote_uri_display, repo.repo_name),
2093 'repository': repo.repo_name
2097 'repository': repo.repo_name
2094 }
2098 }
2095 except Exception:
2099 except Exception:
2096 log.exception("Exception occurred while trying to "
2100 log.exception("Exception occurred while trying to "
2097 "pull changes from remote location")
2101 "pull changes from remote location")
2098 raise JSONRPCError(
2102 raise JSONRPCError(
2099 'Unable to pull changes from `%s`' % remote_uri_display
2103 'Unable to pull changes from `%s`' % remote_uri_display
2100 )
2104 )
2101
2105
2102
2106
2103 @jsonrpc_method()
2107 @jsonrpc_method()
2104 def strip(request, apiuser, repoid, revision, branch):
2108 def strip(request, apiuser, repoid, revision, branch):
2105 """
2109 """
2106 Strips the given revision from the specified repository.
2110 Strips the given revision from the specified repository.
2107
2111
2108 * This will remove the revision and all of its decendants.
2112 * This will remove the revision and all of its decendants.
2109
2113
2110 This command can only be run using an |authtoken| with admin rights to
2114 This command can only be run using an |authtoken| with admin rights to
2111 the specified repository.
2115 the specified repository.
2112
2116
2113 This command takes the following options:
2117 This command takes the following options:
2114
2118
2115 :param apiuser: This is filled automatically from the |authtoken|.
2119 :param apiuser: This is filled automatically from the |authtoken|.
2116 :type apiuser: AuthUser
2120 :type apiuser: AuthUser
2117 :param repoid: The repository name or repository ID.
2121 :param repoid: The repository name or repository ID.
2118 :type repoid: str or int
2122 :type repoid: str or int
2119 :param revision: The revision you wish to strip.
2123 :param revision: The revision you wish to strip.
2120 :type revision: str
2124 :type revision: str
2121 :param branch: The branch from which to strip the revision.
2125 :param branch: The branch from which to strip the revision.
2122 :type branch: str
2126 :type branch: str
2123
2127
2124 Example output:
2128 Example output:
2125
2129
2126 .. code-block:: bash
2130 .. code-block:: bash
2127
2131
2128 id : <id_given_in_input>
2132 id : <id_given_in_input>
2129 result : {
2133 result : {
2130 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2134 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2131 "repository": "<repository name>"
2135 "repository": "<repository name>"
2132 }
2136 }
2133 error : null
2137 error : null
2134
2138
2135 Example error output:
2139 Example error output:
2136
2140
2137 .. code-block:: bash
2141 .. code-block:: bash
2138
2142
2139 id : <id_given_in_input>
2143 id : <id_given_in_input>
2140 result : null
2144 result : null
2141 error : {
2145 error : {
2142 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2146 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2143 }
2147 }
2144
2148
2145 """
2149 """
2146
2150
2147 repo = get_repo_or_error(repoid)
2151 repo = get_repo_or_error(repoid)
2148 if not has_superadmin_permission(apiuser):
2152 if not has_superadmin_permission(apiuser):
2149 _perms = ('repository.admin',)
2153 _perms = ('repository.admin',)
2150 validate_repo_permissions(apiuser, repoid, repo, _perms)
2154 validate_repo_permissions(apiuser, repoid, repo, _perms)
2151
2155
2152 try:
2156 try:
2153 ScmModel().strip(repo, revision, branch)
2157 ScmModel().strip(repo, revision, branch)
2154 audit_logger.store_api(
2158 audit_logger.store_api(
2155 'repo.commit.strip', action_data={'commit_id': revision},
2159 'repo.commit.strip', action_data={'commit_id': revision},
2156 repo=repo,
2160 repo=repo,
2157 user=apiuser, commit=True)
2161 user=apiuser, commit=True)
2158
2162
2159 return {
2163 return {
2160 'msg': 'Stripped commit %s from repo `%s`' % (
2164 'msg': 'Stripped commit %s from repo `%s`' % (
2161 revision, repo.repo_name),
2165 revision, repo.repo_name),
2162 'repository': repo.repo_name
2166 'repository': repo.repo_name
2163 }
2167 }
2164 except Exception:
2168 except Exception:
2165 log.exception("Exception while trying to strip")
2169 log.exception("Exception while trying to strip")
2166 raise JSONRPCError(
2170 raise JSONRPCError(
2167 'Unable to strip commit %s from repo `%s`' % (
2171 'Unable to strip commit %s from repo `%s`' % (
2168 revision, repo.repo_name)
2172 revision, repo.repo_name)
2169 )
2173 )
2170
2174
2171
2175
2172 @jsonrpc_method()
2176 @jsonrpc_method()
2173 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2177 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2174 """
2178 """
2175 Returns all settings for a repository. If key is given it only returns the
2179 Returns all settings for a repository. If key is given it only returns the
2176 setting identified by the key or null.
2180 setting identified by the key or null.
2177
2181
2178 :param apiuser: This is filled automatically from the |authtoken|.
2182 :param apiuser: This is filled automatically from the |authtoken|.
2179 :type apiuser: AuthUser
2183 :type apiuser: AuthUser
2180 :param repoid: The repository name or repository id.
2184 :param repoid: The repository name or repository id.
2181 :type repoid: str or int
2185 :type repoid: str or int
2182 :param key: Key of the setting to return.
2186 :param key: Key of the setting to return.
2183 :type: key: Optional(str)
2187 :type: key: Optional(str)
2184
2188
2185 Example output:
2189 Example output:
2186
2190
2187 .. code-block:: bash
2191 .. code-block:: bash
2188
2192
2189 {
2193 {
2190 "error": null,
2194 "error": null,
2191 "id": 237,
2195 "id": 237,
2192 "result": {
2196 "result": {
2193 "extensions_largefiles": true,
2197 "extensions_largefiles": true,
2194 "extensions_evolve": true,
2198 "extensions_evolve": true,
2195 "hooks_changegroup_push_logger": true,
2199 "hooks_changegroup_push_logger": true,
2196 "hooks_changegroup_repo_size": false,
2200 "hooks_changegroup_repo_size": false,
2197 "hooks_outgoing_pull_logger": true,
2201 "hooks_outgoing_pull_logger": true,
2198 "phases_publish": "True",
2202 "phases_publish": "True",
2199 "rhodecode_hg_use_rebase_for_merging": true,
2203 "rhodecode_hg_use_rebase_for_merging": true,
2200 "rhodecode_pr_merge_enabled": true,
2204 "rhodecode_pr_merge_enabled": true,
2201 "rhodecode_use_outdated_comments": true
2205 "rhodecode_use_outdated_comments": true
2202 }
2206 }
2203 }
2207 }
2204 """
2208 """
2205
2209
2206 # Restrict access to this api method to admins only.
2210 # Restrict access to this api method to admins only.
2207 if not has_superadmin_permission(apiuser):
2211 if not has_superadmin_permission(apiuser):
2208 raise JSONRPCForbidden()
2212 raise JSONRPCForbidden()
2209
2213
2210 try:
2214 try:
2211 repo = get_repo_or_error(repoid)
2215 repo = get_repo_or_error(repoid)
2212 settings_model = VcsSettingsModel(repo=repo)
2216 settings_model = VcsSettingsModel(repo=repo)
2213 settings = settings_model.get_global_settings()
2217 settings = settings_model.get_global_settings()
2214 settings.update(settings_model.get_repo_settings())
2218 settings.update(settings_model.get_repo_settings())
2215
2219
2216 # If only a single setting is requested fetch it from all settings.
2220 # If only a single setting is requested fetch it from all settings.
2217 key = Optional.extract(key)
2221 key = Optional.extract(key)
2218 if key is not None:
2222 if key is not None:
2219 settings = settings.get(key, None)
2223 settings = settings.get(key, None)
2220 except Exception:
2224 except Exception:
2221 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2225 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2222 log.exception(msg)
2226 log.exception(msg)
2223 raise JSONRPCError(msg)
2227 raise JSONRPCError(msg)
2224
2228
2225 return settings
2229 return settings
2226
2230
2227
2231
2228 @jsonrpc_method()
2232 @jsonrpc_method()
2229 def set_repo_settings(request, apiuser, repoid, settings):
2233 def set_repo_settings(request, apiuser, repoid, settings):
2230 """
2234 """
2231 Update repository settings. Returns true on success.
2235 Update repository settings. Returns true on success.
2232
2236
2233 :param apiuser: This is filled automatically from the |authtoken|.
2237 :param apiuser: This is filled automatically from the |authtoken|.
2234 :type apiuser: AuthUser
2238 :type apiuser: AuthUser
2235 :param repoid: The repository name or repository id.
2239 :param repoid: The repository name or repository id.
2236 :type repoid: str or int
2240 :type repoid: str or int
2237 :param settings: The new settings for the repository.
2241 :param settings: The new settings for the repository.
2238 :type: settings: dict
2242 :type: settings: dict
2239
2243
2240 Example output:
2244 Example output:
2241
2245
2242 .. code-block:: bash
2246 .. code-block:: bash
2243
2247
2244 {
2248 {
2245 "error": null,
2249 "error": null,
2246 "id": 237,
2250 "id": 237,
2247 "result": true
2251 "result": true
2248 }
2252 }
2249 """
2253 """
2250 # Restrict access to this api method to admins only.
2254 # Restrict access to this api method to admins only.
2251 if not has_superadmin_permission(apiuser):
2255 if not has_superadmin_permission(apiuser):
2252 raise JSONRPCForbidden()
2256 raise JSONRPCForbidden()
2253
2257
2254 if type(settings) is not dict:
2258 if type(settings) is not dict:
2255 raise JSONRPCError('Settings have to be a JSON Object.')
2259 raise JSONRPCError('Settings have to be a JSON Object.')
2256
2260
2257 try:
2261 try:
2258 settings_model = VcsSettingsModel(repo=repoid)
2262 settings_model = VcsSettingsModel(repo=repoid)
2259
2263
2260 # Merge global, repo and incoming settings.
2264 # Merge global, repo and incoming settings.
2261 new_settings = settings_model.get_global_settings()
2265 new_settings = settings_model.get_global_settings()
2262 new_settings.update(settings_model.get_repo_settings())
2266 new_settings.update(settings_model.get_repo_settings())
2263 new_settings.update(settings)
2267 new_settings.update(settings)
2264
2268
2265 # Update the settings.
2269 # Update the settings.
2266 inherit_global_settings = new_settings.get(
2270 inherit_global_settings = new_settings.get(
2267 'inherit_global_settings', False)
2271 'inherit_global_settings', False)
2268 settings_model.create_or_update_repo_settings(
2272 settings_model.create_or_update_repo_settings(
2269 new_settings, inherit_global_settings=inherit_global_settings)
2273 new_settings, inherit_global_settings=inherit_global_settings)
2270 Session().commit()
2274 Session().commit()
2271 except Exception:
2275 except Exception:
2272 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2276 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2273 log.exception(msg)
2277 log.exception(msg)
2274 raise JSONRPCError(msg)
2278 raise JSONRPCError(msg)
2275
2279
2276 # Indicate success.
2280 # Indicate success.
2277 return True
2281 return True
2278
2282
2279
2283
2280 @jsonrpc_method()
2284 @jsonrpc_method()
2281 def maintenance(request, apiuser, repoid):
2285 def maintenance(request, apiuser, repoid):
2282 """
2286 """
2283 Triggers a maintenance on the given repository.
2287 Triggers a maintenance on the given repository.
2284
2288
2285 This command can only be run using an |authtoken| with admin
2289 This command can only be run using an |authtoken| with admin
2286 rights to the specified repository. For more information,
2290 rights to the specified repository. For more information,
2287 see :ref:`config-token-ref`.
2291 see :ref:`config-token-ref`.
2288
2292
2289 This command takes the following options:
2293 This command takes the following options:
2290
2294
2291 :param apiuser: This is filled automatically from the |authtoken|.
2295 :param apiuser: This is filled automatically from the |authtoken|.
2292 :type apiuser: AuthUser
2296 :type apiuser: AuthUser
2293 :param repoid: The repository name or repository ID.
2297 :param repoid: The repository name or repository ID.
2294 :type repoid: str or int
2298 :type repoid: str or int
2295
2299
2296 Example output:
2300 Example output:
2297
2301
2298 .. code-block:: bash
2302 .. code-block:: bash
2299
2303
2300 id : <id_given_in_input>
2304 id : <id_given_in_input>
2301 result : {
2305 result : {
2302 "msg": "executed maintenance command",
2306 "msg": "executed maintenance command",
2303 "executed_actions": [
2307 "executed_actions": [
2304 <action_message>, <action_message2>...
2308 <action_message>, <action_message2>...
2305 ],
2309 ],
2306 "repository": "<repository name>"
2310 "repository": "<repository name>"
2307 }
2311 }
2308 error : null
2312 error : null
2309
2313
2310 Example error output:
2314 Example error output:
2311
2315
2312 .. code-block:: bash
2316 .. code-block:: bash
2313
2317
2314 id : <id_given_in_input>
2318 id : <id_given_in_input>
2315 result : null
2319 result : null
2316 error : {
2320 error : {
2317 "Unable to execute maintenance on `<reponame>`"
2321 "Unable to execute maintenance on `<reponame>`"
2318 }
2322 }
2319
2323
2320 """
2324 """
2321
2325
2322 repo = get_repo_or_error(repoid)
2326 repo = get_repo_or_error(repoid)
2323 if not has_superadmin_permission(apiuser):
2327 if not has_superadmin_permission(apiuser):
2324 _perms = ('repository.admin',)
2328 _perms = ('repository.admin',)
2325 validate_repo_permissions(apiuser, repoid, repo, _perms)
2329 validate_repo_permissions(apiuser, repoid, repo, _perms)
2326
2330
2327 try:
2331 try:
2328 maintenance = repo_maintenance.RepoMaintenance()
2332 maintenance = repo_maintenance.RepoMaintenance()
2329 executed_actions = maintenance.execute(repo)
2333 executed_actions = maintenance.execute(repo)
2330
2334
2331 return {
2335 return {
2332 'msg': 'executed maintenance command',
2336 'msg': 'executed maintenance command',
2333 'executed_actions': executed_actions,
2337 'executed_actions': executed_actions,
2334 'repository': repo.repo_name
2338 'repository': repo.repo_name
2335 }
2339 }
2336 except Exception:
2340 except Exception:
2337 log.exception("Exception occurred while trying to run maintenance")
2341 log.exception("Exception occurred while trying to run maintenance")
2338 raise JSONRPCError(
2342 raise JSONRPCError(
2339 'Unable to execute maintenance on `%s`' % repo.repo_name)
2343 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,419 +1,421 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
22 import logging
21 import logging
23 import itertools
22 import itertools
24 import base64
23 import base64
25
24
26 from pyramid import compat
25 from pyramid import compat
27
26
28 from rhodecode.api import (
27 from rhodecode.api import (
29 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
28 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
30
29
31 from rhodecode.api.utils import (
30 from rhodecode.api.utils import (
32 Optional, OAttr, has_superadmin_permission, get_user_or_error)
31 Optional, OAttr, has_superadmin_permission, get_user_or_error)
33 from rhodecode.lib.utils import repo2db_mapper
32 from rhodecode.lib.utils import repo2db_mapper
34 from rhodecode.lib import system_info
33 from rhodecode.lib import system_info
35 from rhodecode.lib import user_sessions
34 from rhodecode.lib import user_sessions
36 from rhodecode.lib import exc_tracking
35 from rhodecode.lib import exc_tracking
37 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.utils2 import safe_int
37 from rhodecode.lib.utils2 import safe_int
39 from rhodecode.model.db import UserIpMap
38 from rhodecode.model.db import UserIpMap
40 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.settings import VcsSettingsModel
40 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.apps.file_store import utils
41 from rhodecode.apps.file_store import utils
43 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
42 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
44 FileOverSizeException
43 FileOverSizeException
45
44
46 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
47
46
48
47
49 @jsonrpc_method()
48 @jsonrpc_method()
50 def get_server_info(request, apiuser):
49 def get_server_info(request, apiuser):
51 """
50 """
52 Returns the |RCE| server information.
51 Returns the |RCE| server information.
53
52
54 This includes the running version of |RCE| and all installed
53 This includes the running version of |RCE| and all installed
55 packages. This command takes the following options:
54 packages. This command takes the following options:
56
55
57 :param apiuser: This is filled automatically from the |authtoken|.
56 :param apiuser: This is filled automatically from the |authtoken|.
58 :type apiuser: AuthUser
57 :type apiuser: AuthUser
59
58
60 Example output:
59 Example output:
61
60
62 .. code-block:: bash
61 .. code-block:: bash
63
62
64 id : <id_given_in_input>
63 id : <id_given_in_input>
65 result : {
64 result : {
66 'modules': [<module name>,...]
65 'modules': [<module name>,...]
67 'py_version': <python version>,
66 'py_version': <python version>,
68 'platform': <platform type>,
67 'platform': <platform type>,
69 'rhodecode_version': <rhodecode version>
68 'rhodecode_version': <rhodecode version>
70 }
69 }
71 error : null
70 error : null
72 """
71 """
73
72
74 if not has_superadmin_permission(apiuser):
73 if not has_superadmin_permission(apiuser):
75 raise JSONRPCForbidden()
74 raise JSONRPCForbidden()
76
75
77 server_info = ScmModel().get_server_info(request.environ)
76 server_info = ScmModel().get_server_info(request.environ)
78 # rhodecode-index requires those
77 # rhodecode-index requires those
79
78
80 server_info['index_storage'] = server_info['search']['value']['location']
79 server_info['index_storage'] = server_info['search']['value']['location']
81 server_info['storage'] = server_info['storage']['value']['path']
80 server_info['storage'] = server_info['storage']['value']['path']
82
81
83 return server_info
82 return server_info
84
83
85
84
86 @jsonrpc_method()
85 @jsonrpc_method()
87 def get_repo_store(request, apiuser):
86 def get_repo_store(request, apiuser):
88 """
87 """
89 Returns the |RCE| repository storage information.
88 Returns the |RCE| repository storage information.
90
89
91 :param apiuser: This is filled automatically from the |authtoken|.
90 :param apiuser: This is filled automatically from the |authtoken|.
92 :type apiuser: AuthUser
91 :type apiuser: AuthUser
93
92
94 Example output:
93 Example output:
95
94
96 .. code-block:: bash
95 .. code-block:: bash
97
96
98 id : <id_given_in_input>
97 id : <id_given_in_input>
99 result : {
98 result : {
100 'modules': [<module name>,...]
99 'modules': [<module name>,...]
101 'py_version': <python version>,
100 'py_version': <python version>,
102 'platform': <platform type>,
101 'platform': <platform type>,
103 'rhodecode_version': <rhodecode version>
102 'rhodecode_version': <rhodecode version>
104 }
103 }
105 error : null
104 error : null
106 """
105 """
107
106
108 if not has_superadmin_permission(apiuser):
107 if not has_superadmin_permission(apiuser):
109 raise JSONRPCForbidden()
108 raise JSONRPCForbidden()
110
109
111 path = VcsSettingsModel().get_repos_location()
110 path = VcsSettingsModel().get_repos_location()
112 return {"path": path}
111 return {"path": path}
113
112
114
113
115 @jsonrpc_method()
114 @jsonrpc_method()
116 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
115 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
117 """
116 """
118 Displays the IP Address as seen from the |RCE| server.
117 Displays the IP Address as seen from the |RCE| server.
119
118
120 * This command displays the IP Address, as well as all the defined IP
119 * This command displays the IP Address, as well as all the defined IP
121 addresses for the specified user. If the ``userid`` is not set, the
120 addresses for the specified user. If the ``userid`` is not set, the
122 data returned is for the user calling the method.
121 data returned is for the user calling the method.
123
122
124 This command can only be run using an |authtoken| with admin rights to
123 This command can only be run using an |authtoken| with admin rights to
125 the specified repository.
124 the specified repository.
126
125
127 This command takes the following options:
126 This command takes the following options:
128
127
129 :param apiuser: This is filled automatically from |authtoken|.
128 :param apiuser: This is filled automatically from |authtoken|.
130 :type apiuser: AuthUser
129 :type apiuser: AuthUser
131 :param userid: Sets the userid for which associated IP Address data
130 :param userid: Sets the userid for which associated IP Address data
132 is returned.
131 is returned.
133 :type userid: Optional(str or int)
132 :type userid: Optional(str or int)
134
133
135 Example output:
134 Example output:
136
135
137 .. code-block:: bash
136 .. code-block:: bash
138
137
139 id : <id_given_in_input>
138 id : <id_given_in_input>
140 result : {
139 result : {
141 "server_ip_addr": "<ip_from_clien>",
140 "server_ip_addr": "<ip_from_clien>",
142 "user_ips": [
141 "user_ips": [
143 {
142 {
144 "ip_addr": "<ip_with_mask>",
143 "ip_addr": "<ip_with_mask>",
145 "ip_range": ["<start_ip>", "<end_ip>"],
144 "ip_range": ["<start_ip>", "<end_ip>"],
146 },
145 },
147 ...
146 ...
148 ]
147 ]
149 }
148 }
150
149
151 """
150 """
152 if not has_superadmin_permission(apiuser):
151 if not has_superadmin_permission(apiuser):
153 raise JSONRPCForbidden()
152 raise JSONRPCForbidden()
154
153
155 userid = Optional.extract(userid, evaluate_locals=locals())
154 userid = Optional.extract(userid, evaluate_locals=locals())
156 userid = getattr(userid, 'user_id', userid)
155 userid = getattr(userid, 'user_id', userid)
157
156
158 user = get_user_or_error(userid)
157 user = get_user_or_error(userid)
159 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
158 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
160 return {
159 return {
161 'server_ip_addr': request.rpc_ip_addr,
160 'server_ip_addr': request.rpc_ip_addr,
162 'user_ips': ips
161 'user_ips': ips
163 }
162 }
164
163
165
164
166 @jsonrpc_method()
165 @jsonrpc_method()
167 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
166 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
168 """
167 """
169 Triggers a rescan of the specified repositories.
168 Triggers a rescan of the specified repositories.
170
169
171 * If the ``remove_obsolete`` option is set, it also deletes repositories
170 * If the ``remove_obsolete`` option is set, it also deletes repositories
172 that are found in the database but not on the file system, so called
171 that are found in the database but not on the file system, so called
173 "clean zombies".
172 "clean zombies".
174
173
175 This command can only be run using an |authtoken| with admin rights to
174 This command can only be run using an |authtoken| with admin rights to
176 the specified repository.
175 the specified repository.
177
176
178 This command takes the following options:
177 This command takes the following options:
179
178
180 :param apiuser: This is filled automatically from the |authtoken|.
179 :param apiuser: This is filled automatically from the |authtoken|.
181 :type apiuser: AuthUser
180 :type apiuser: AuthUser
182 :param remove_obsolete: Deletes repositories from the database that
181 :param remove_obsolete: Deletes repositories from the database that
183 are not found on the filesystem.
182 are not found on the filesystem.
184 :type remove_obsolete: Optional(``True`` | ``False``)
183 :type remove_obsolete: Optional(``True`` | ``False``)
185
184
186 Example output:
185 Example output:
187
186
188 .. code-block:: bash
187 .. code-block:: bash
189
188
190 id : <id_given_in_input>
189 id : <id_given_in_input>
191 result : {
190 result : {
192 'added': [<added repository name>,...]
191 'added': [<added repository name>,...]
193 'removed': [<removed repository name>,...]
192 'removed': [<removed repository name>,...]
194 }
193 }
195 error : null
194 error : null
196
195
197 Example error output:
196 Example error output:
198
197
199 .. code-block:: bash
198 .. code-block:: bash
200
199
201 id : <id_given_in_input>
200 id : <id_given_in_input>
202 result : null
201 result : null
203 error : {
202 error : {
204 'Error occurred during rescan repositories action'
203 'Error occurred during rescan repositories action'
205 }
204 }
206
205
207 """
206 """
208 if not has_superadmin_permission(apiuser):
207 if not has_superadmin_permission(apiuser):
209 raise JSONRPCForbidden()
208 raise JSONRPCForbidden()
210
209
211 try:
210 try:
212 rm_obsolete = Optional.extract(remove_obsolete)
211 rm_obsolete = Optional.extract(remove_obsolete)
213 added, removed = repo2db_mapper(ScmModel().repo_scan(),
212 added, removed = repo2db_mapper(ScmModel().repo_scan(),
214 remove_obsolete=rm_obsolete)
213 remove_obsolete=rm_obsolete)
215 return {'added': added, 'removed': removed}
214 return {'added': added, 'removed': removed}
216 except Exception:
215 except Exception:
217 log.exception('Failed to run repo rescann')
216 log.exception('Failed to run repo rescann')
218 raise JSONRPCError(
217 raise JSONRPCError(
219 'Error occurred during rescan repositories action'
218 'Error occurred during rescan repositories action'
220 )
219 )
221
220
222
221
223 @jsonrpc_method()
222 @jsonrpc_method()
224 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
223 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
225 """
224 """
226 Triggers a session cleanup action.
225 Triggers a session cleanup action.
227
226
228 If the ``older_then`` option is set, only sessions that hasn't been
227 If the ``older_then`` option is set, only sessions that hasn't been
229 accessed in the given number of days will be removed.
228 accessed in the given number of days will be removed.
230
229
231 This command can only be run using an |authtoken| with admin rights to
230 This command can only be run using an |authtoken| with admin rights to
232 the specified repository.
231 the specified repository.
233
232
234 This command takes the following options:
233 This command takes the following options:
235
234
236 :param apiuser: This is filled automatically from the |authtoken|.
235 :param apiuser: This is filled automatically from the |authtoken|.
237 :type apiuser: AuthUser
236 :type apiuser: AuthUser
238 :param older_then: Deletes session that hasn't been accessed
237 :param older_then: Deletes session that hasn't been accessed
239 in given number of days.
238 in given number of days.
240 :type older_then: Optional(int)
239 :type older_then: Optional(int)
241
240
242 Example output:
241 Example output:
243
242
244 .. code-block:: bash
243 .. code-block:: bash
245
244
246 id : <id_given_in_input>
245 id : <id_given_in_input>
247 result: {
246 result: {
248 "backend": "<type of backend>",
247 "backend": "<type of backend>",
249 "sessions_removed": <number_of_removed_sessions>
248 "sessions_removed": <number_of_removed_sessions>
250 }
249 }
251 error : null
250 error : null
252
251
253 Example error output:
252 Example error output:
254
253
255 .. code-block:: bash
254 .. code-block:: bash
256
255
257 id : <id_given_in_input>
256 id : <id_given_in_input>
258 result : null
257 result : null
259 error : {
258 error : {
260 'Error occurred during session cleanup'
259 'Error occurred during session cleanup'
261 }
260 }
262
261
263 """
262 """
264 if not has_superadmin_permission(apiuser):
263 if not has_superadmin_permission(apiuser):
265 raise JSONRPCForbidden()
264 raise JSONRPCForbidden()
266
265
267 older_then = safe_int(Optional.extract(older_then)) or 60
266 older_then = safe_int(Optional.extract(older_then)) or 60
268 older_than_seconds = 60 * 60 * 24 * older_then
267 older_than_seconds = 60 * 60 * 24 * older_then
269
268
270 config = system_info.rhodecode_config().get_value()['value']['config']
269 config = system_info.rhodecode_config().get_value()['value']['config']
271 session_model = user_sessions.get_session_handler(
270 session_model = user_sessions.get_session_handler(
272 config.get('beaker.session.type', 'memory'))(config)
271 config.get('beaker.session.type', 'memory'))(config)
273
272
274 backend = session_model.SESSION_TYPE
273 backend = session_model.SESSION_TYPE
275 try:
274 try:
276 cleaned = session_model.clean_sessions(
275 cleaned = session_model.clean_sessions(
277 older_than_seconds=older_than_seconds)
276 older_than_seconds=older_than_seconds)
278 return {'sessions_removed': cleaned, 'backend': backend}
277 return {'sessions_removed': cleaned, 'backend': backend}
279 except user_sessions.CleanupCommand as msg:
278 except user_sessions.CleanupCommand as msg:
280 return {'cleanup_command': msg.message, 'backend': backend}
279 return {'cleanup_command': msg.message, 'backend': backend}
281 except Exception as e:
280 except Exception as e:
282 log.exception('Failed session cleanup')
281 log.exception('Failed session cleanup')
283 raise JSONRPCError(
282 raise JSONRPCError(
284 'Error occurred during session cleanup'
283 'Error occurred during session cleanup'
285 )
284 )
286
285
287
286
288 @jsonrpc_method()
287 @jsonrpc_method()
289 def get_method(request, apiuser, pattern=Optional('*')):
288 def get_method(request, apiuser, pattern=Optional('*')):
290 """
289 """
291 Returns list of all available API methods. By default match pattern
290 Returns list of all available API methods. By default match pattern
292 os "*" but any other pattern can be specified. eg *comment* will return
291 os "*" but any other pattern can be specified. eg *comment* will return
293 all methods with comment inside them. If just single method is matched
292 all methods with comment inside them. If just single method is matched
294 returned data will also include method specification
293 returned data will also include method specification
295
294
296 This command can only be run using an |authtoken| with admin rights to
295 This command can only be run using an |authtoken| with admin rights to
297 the specified repository.
296 the specified repository.
298
297
299 This command takes the following options:
298 This command takes the following options:
300
299
301 :param apiuser: This is filled automatically from the |authtoken|.
300 :param apiuser: This is filled automatically from the |authtoken|.
302 :type apiuser: AuthUser
301 :type apiuser: AuthUser
303 :param pattern: pattern to match method names against
302 :param pattern: pattern to match method names against
304 :type pattern: Optional("*")
303 :type pattern: Optional("*")
305
304
306 Example output:
305 Example output:
307
306
308 .. code-block:: bash
307 .. code-block:: bash
309
308
310 id : <id_given_in_input>
309 id : <id_given_in_input>
311 "result": [
310 "result": [
312 "changeset_comment",
311 "changeset_comment",
313 "comment_pull_request",
312 "comment_pull_request",
314 "comment_commit"
313 "comment_commit"
315 ]
314 ]
316 error : null
315 error : null
317
316
318 .. code-block:: bash
317 .. code-block:: bash
319
318
320 id : <id_given_in_input>
319 id : <id_given_in_input>
321 "result": [
320 "result": [
322 "comment_commit",
321 "comment_commit",
323 {
322 {
324 "apiuser": "<RequiredType>",
323 "apiuser": "<RequiredType>",
325 "comment_type": "<Optional:u'note'>",
324 "comment_type": "<Optional:u'note'>",
326 "commit_id": "<RequiredType>",
325 "commit_id": "<RequiredType>",
327 "message": "<RequiredType>",
326 "message": "<RequiredType>",
328 "repoid": "<RequiredType>",
327 "repoid": "<RequiredType>",
329 "request": "<RequiredType>",
328 "request": "<RequiredType>",
330 "resolves_comment_id": "<Optional:None>",
329 "resolves_comment_id": "<Optional:None>",
331 "status": "<Optional:None>",
330 "status": "<Optional:None>",
332 "userid": "<Optional:<OptionalAttr:apiuser>>"
331 "userid": "<Optional:<OptionalAttr:apiuser>>"
333 }
332 }
334 ]
333 ]
335 error : null
334 error : null
336 """
335 """
336 from rhodecode.config.patches import inspect_getargspec
337 inspect = inspect_getargspec()
338
337 if not has_superadmin_permission(apiuser):
339 if not has_superadmin_permission(apiuser):
338 raise JSONRPCForbidden()
340 raise JSONRPCForbidden()
339
341
340 pattern = Optional.extract(pattern)
342 pattern = Optional.extract(pattern)
341
343
342 matches = find_methods(request.registry.jsonrpc_methods, pattern)
344 matches = find_methods(request.registry.jsonrpc_methods, pattern)
343
345
344 args_desc = []
346 args_desc = []
345 if len(matches) == 1:
347 if len(matches) == 1:
346 func = matches[matches.keys()[0]]
348 func = matches[matches.keys()[0]]
347
349
348 argspec = inspect.getargspec(func)
350 argspec = inspect.getargspec(func)
349 arglist = argspec[0]
351 arglist = argspec[0]
350 defaults = map(repr, argspec[3] or [])
352 defaults = map(repr, argspec[3] or [])
351
353
352 default_empty = '<RequiredType>'
354 default_empty = '<RequiredType>'
353
355
354 # kw arguments required by this method
356 # kw arguments required by this method
355 func_kwargs = dict(itertools.izip_longest(
357 func_kwargs = dict(itertools.izip_longest(
356 reversed(arglist), reversed(defaults), fillvalue=default_empty))
358 reversed(arglist), reversed(defaults), fillvalue=default_empty))
357 args_desc.append(func_kwargs)
359 args_desc.append(func_kwargs)
358
360
359 return matches.keys() + args_desc
361 return matches.keys() + args_desc
360
362
361
363
362 @jsonrpc_method()
364 @jsonrpc_method()
363 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
365 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
364 """
366 """
365 Stores sent exception inside the built-in exception tracker in |RCE| server.
367 Stores sent exception inside the built-in exception tracker in |RCE| server.
366
368
367 This command can only be run using an |authtoken| with admin rights to
369 This command can only be run using an |authtoken| with admin rights to
368 the specified repository.
370 the specified repository.
369
371
370 This command takes the following options:
372 This command takes the following options:
371
373
372 :param apiuser: This is filled automatically from the |authtoken|.
374 :param apiuser: This is filled automatically from the |authtoken|.
373 :type apiuser: AuthUser
375 :type apiuser: AuthUser
374
376
375 :param exc_data_json: JSON data with exception e.g
377 :param exc_data_json: JSON data with exception e.g
376 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
378 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
377 :type exc_data_json: JSON data
379 :type exc_data_json: JSON data
378
380
379 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
381 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
380 :type prefix: Optional("rhodecode")
382 :type prefix: Optional("rhodecode")
381
383
382 Example output:
384 Example output:
383
385
384 .. code-block:: bash
386 .. code-block:: bash
385
387
386 id : <id_given_in_input>
388 id : <id_given_in_input>
387 "result": {
389 "result": {
388 "exc_id": 139718459226384,
390 "exc_id": 139718459226384,
389 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
391 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
390 }
392 }
391 error : null
393 error : null
392 """
394 """
393 if not has_superadmin_permission(apiuser):
395 if not has_superadmin_permission(apiuser):
394 raise JSONRPCForbidden()
396 raise JSONRPCForbidden()
395
397
396 prefix = Optional.extract(prefix)
398 prefix = Optional.extract(prefix)
397 exc_id = exc_tracking.generate_id()
399 exc_id = exc_tracking.generate_id()
398
400
399 try:
401 try:
400 exc_data = json.loads(exc_data_json)
402 exc_data = json.loads(exc_data_json)
401 except Exception:
403 except Exception:
402 log.error('Failed to parse JSON: %r', exc_data_json)
404 log.error('Failed to parse JSON: %r', exc_data_json)
403 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
405 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
404 'Please make sure it contains a valid JSON.')
406 'Please make sure it contains a valid JSON.')
405
407
406 try:
408 try:
407 exc_traceback = exc_data['exc_traceback']
409 exc_traceback = exc_data['exc_traceback']
408 exc_type_name = exc_data['exc_type_name']
410 exc_type_name = exc_data['exc_type_name']
409 except KeyError as err:
411 except KeyError as err:
410 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
412 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
411 'in exc_data_json field. Missing: {}'.format(err))
413 'in exc_data_json field. Missing: {}'.format(err))
412
414
413 exc_tracking._store_exception(
415 exc_tracking._store_exception(
414 exc_id=exc_id, exc_traceback=exc_traceback,
416 exc_id=exc_id, exc_traceback=exc_traceback,
415 exc_type_name=exc_type_name, prefix=prefix)
417 exc_type_name=exc_type_name, prefix=prefix)
416
418
417 exc_url = request.route_url(
419 exc_url = request.route_url(
418 'admin_settings_exception_tracker_show', exception_id=exc_id)
420 'admin_settings_exception_tracker_show', exception_id=exc_id)
419 return {'exc_id': exc_id, 'exc_url': exc_url}
421 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,802 +1,803 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 import operator
23 import operator
24
24
25 from pyramid import compat
25 from pyramid import compat
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27
27
28 from rhodecode.lib import helpers as h, diffs, rc_cache
28 from rhodecode.lib import helpers as h, diffs, rc_cache
29 from rhodecode.lib.utils2 import (
29 from rhodecode.lib.utils2 import (
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
34 from rhodecode.model import repo
34 from rhodecode.model import repo
35 from rhodecode.model import repo_group
35 from rhodecode.model import repo_group
36 from rhodecode.model import user_group
36 from rhodecode.model import user_group
37 from rhodecode.model import user
37 from rhodecode.model import user
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.settings import VcsSettingsModel
40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
41 from rhodecode.model.repo import ReadmeFinder
41 from rhodecode.model.repo import ReadmeFinder
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 ADMIN_PREFIX = '/_admin'
46 ADMIN_PREFIX = '/_admin'
47 STATIC_FILE_PREFIX = '/_static'
47 STATIC_FILE_PREFIX = '/_static'
48
48
49 URL_NAME_REQUIREMENTS = {
49 URL_NAME_REQUIREMENTS = {
50 # group name can have a slash in them, but they must not end with a slash
50 # group name can have a slash in them, but they must not end with a slash
51 'group_name': r'.*?[^/]',
51 'group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
53 # repo names can have a slash in them, but they must not end with a slash
53 # repo names can have a slash in them, but they must not end with a slash
54 'repo_name': r'.*?[^/]',
54 'repo_name': r'.*?[^/]',
55 # file path eats up everything at the end
55 # file path eats up everything at the end
56 'f_path': r'.*',
56 'f_path': r'.*',
57 # reference types
57 # reference types
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
60 }
60 }
61
61
62
62
63 def add_route_with_slash(config,name, pattern, **kw):
63 def add_route_with_slash(config,name, pattern, **kw):
64 config.add_route(name, pattern, **kw)
64 config.add_route(name, pattern, **kw)
65 if not pattern.endswith('/'):
65 if not pattern.endswith('/'):
66 config.add_route(name + '_slash', pattern + '/', **kw)
66 config.add_route(name + '_slash', pattern + '/', **kw)
67
67
68
68
69 def add_route_requirements(route_path, requirements=None):
69 def add_route_requirements(route_path, requirements=None):
70 """
70 """
71 Adds regex requirements to pyramid routes using a mapping dict
71 Adds regex requirements to pyramid routes using a mapping dict
72 e.g::
72 e.g::
73 add_route_requirements('{repo_name}/settings')
73 add_route_requirements('{repo_name}/settings')
74 """
74 """
75 requirements = requirements or URL_NAME_REQUIREMENTS
75 requirements = requirements or URL_NAME_REQUIREMENTS
76 for key, regex in requirements.items():
76 for key, regex in requirements.items():
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
78 return route_path
78 return route_path
79
79
80
80
81 def get_format_ref_id(repo):
81 def get_format_ref_id(repo):
82 """Returns a `repo` specific reference formatter function"""
82 """Returns a `repo` specific reference formatter function"""
83 if h.is_svn(repo):
83 if h.is_svn(repo):
84 return _format_ref_id_svn
84 return _format_ref_id_svn
85 else:
85 else:
86 return _format_ref_id
86 return _format_ref_id
87
87
88
88
89 def _format_ref_id(name, raw_id):
89 def _format_ref_id(name, raw_id):
90 """Default formatting of a given reference `name`"""
90 """Default formatting of a given reference `name`"""
91 return name
91 return name
92
92
93
93
94 def _format_ref_id_svn(name, raw_id):
94 def _format_ref_id_svn(name, raw_id):
95 """Special way of formatting a reference for Subversion including path"""
95 """Special way of formatting a reference for Subversion including path"""
96 return '%s@%s' % (name, raw_id)
96 return '%s@%s' % (name, raw_id)
97
97
98
98
99 class TemplateArgs(StrictAttributeDict):
99 class TemplateArgs(StrictAttributeDict):
100 pass
100 pass
101
101
102
102
103 class BaseAppView(object):
103 class BaseAppView(object):
104
104
105 def __init__(self, context, request):
105 def __init__(self, context, request):
106 self.request = request
106 self.request = request
107 self.context = context
107 self.context = context
108 self.session = request.session
108 self.session = request.session
109 if not hasattr(request, 'user'):
109 if not hasattr(request, 'user'):
110 # NOTE(marcink): edge case, we ended up in matched route
110 # NOTE(marcink): edge case, we ended up in matched route
111 # but probably of web-app context, e.g API CALL/VCS CALL
111 # but probably of web-app context, e.g API CALL/VCS CALL
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
113 log.warning('Unable to process request `%s` in this scope', request)
113 log.warning('Unable to process request `%s` in this scope', request)
114 raise HTTPBadRequest()
114 raise HTTPBadRequest()
115
115
116 self._rhodecode_user = request.user # auth user
116 self._rhodecode_user = request.user # auth user
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
118 self._maybe_needs_password_change(
118 self._maybe_needs_password_change(
119 request.matched_route.name, self._rhodecode_db_user)
119 request.matched_route.name, self._rhodecode_db_user)
120
120
121 def _maybe_needs_password_change(self, view_name, user_obj):
121 def _maybe_needs_password_change(self, view_name, user_obj):
122 log.debug('Checking if user %s needs password change on view %s',
122 log.debug('Checking if user %s needs password change on view %s',
123 user_obj, view_name)
123 user_obj, view_name)
124 skip_user_views = [
124 skip_user_views = [
125 'logout', 'login',
125 'logout', 'login',
126 'my_account_password', 'my_account_password_update'
126 'my_account_password', 'my_account_password_update'
127 ]
127 ]
128
128
129 if not user_obj:
129 if not user_obj:
130 return
130 return
131
131
132 if user_obj.username == User.DEFAULT_USER:
132 if user_obj.username == User.DEFAULT_USER:
133 return
133 return
134
134
135 now = time.time()
135 now = time.time()
136 should_change = user_obj.user_data.get('force_password_change')
136 should_change = user_obj.user_data.get('force_password_change')
137 change_after = safe_int(should_change) or 0
137 change_after = safe_int(should_change) or 0
138 if should_change and now > change_after:
138 if should_change and now > change_after:
139 log.debug('User %s requires password change', user_obj)
139 log.debug('User %s requires password change', user_obj)
140 h.flash('You are required to change your password', 'warning',
140 h.flash('You are required to change your password', 'warning',
141 ignore_duplicate=True)
141 ignore_duplicate=True)
142
142
143 if view_name not in skip_user_views:
143 if view_name not in skip_user_views:
144 raise HTTPFound(
144 raise HTTPFound(
145 self.request.route_path('my_account_password'))
145 self.request.route_path('my_account_password'))
146
146
147 def _log_creation_exception(self, e, repo_name):
147 def _log_creation_exception(self, e, repo_name):
148 _ = self.request.translate
148 _ = self.request.translate
149 reason = None
149 reason = None
150 if len(e.args) == 2:
150 if len(e.args) == 2:
151 reason = e.args[1]
151 reason = e.args[1]
152
152
153 if reason == 'INVALID_CERTIFICATE':
153 if reason == 'INVALID_CERTIFICATE':
154 log.exception(
154 log.exception(
155 'Exception creating a repository: invalid certificate')
155 'Exception creating a repository: invalid certificate')
156 msg = (_('Error creating repository %s: invalid certificate')
156 msg = (_('Error creating repository %s: invalid certificate')
157 % repo_name)
157 % repo_name)
158 else:
158 else:
159 log.exception("Exception creating a repository")
159 log.exception("Exception creating a repository")
160 msg = (_('Error creating repository %s')
160 msg = (_('Error creating repository %s')
161 % repo_name)
161 % repo_name)
162 return msg
162 return msg
163
163
164 def _get_local_tmpl_context(self, include_app_defaults=True):
164 def _get_local_tmpl_context(self, include_app_defaults=True):
165 c = TemplateArgs()
165 c = TemplateArgs()
166 c.auth_user = self.request.user
166 c.auth_user = self.request.user
167 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
167 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
168 c.rhodecode_user = self.request.user
168 c.rhodecode_user = self.request.user
169
169
170 if include_app_defaults:
170 if include_app_defaults:
171 from rhodecode.lib.base import attach_context_attributes
171 from rhodecode.lib.base import attach_context_attributes
172 attach_context_attributes(c, self.request, self.request.user.user_id)
172 attach_context_attributes(c, self.request, self.request.user.user_id)
173
173
174 c.is_super_admin = c.auth_user.is_admin
174 c.is_super_admin = c.auth_user.is_admin
175
175
176 c.can_create_repo = c.is_super_admin
176 c.can_create_repo = c.is_super_admin
177 c.can_create_repo_group = c.is_super_admin
177 c.can_create_repo_group = c.is_super_admin
178 c.can_create_user_group = c.is_super_admin
178 c.can_create_user_group = c.is_super_admin
179
179
180 c.is_delegated_admin = False
180 c.is_delegated_admin = False
181
181
182 if not c.auth_user.is_default and not c.is_super_admin:
182 if not c.auth_user.is_default and not c.is_super_admin:
183 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
183 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
184 user=self.request.user)
184 user=self.request.user)
185 repositories = c.auth_user.repositories_admin or c.can_create_repo
185 repositories = c.auth_user.repositories_admin or c.can_create_repo
186
186
187 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
187 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
188 user=self.request.user)
188 user=self.request.user)
189 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
189 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
190
190
191 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
191 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
192 user=self.request.user)
192 user=self.request.user)
193 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
193 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
194 # delegated admin can create, or manage some objects
194 # delegated admin can create, or manage some objects
195 c.is_delegated_admin = repositories or repository_groups or user_groups
195 c.is_delegated_admin = repositories or repository_groups or user_groups
196 return c
196 return c
197
197
198 def _get_template_context(self, tmpl_args, **kwargs):
198 def _get_template_context(self, tmpl_args, **kwargs):
199
199
200 local_tmpl_args = {
200 local_tmpl_args = {
201 'defaults': {},
201 'defaults': {},
202 'errors': {},
202 'errors': {},
203 'c': tmpl_args
203 'c': tmpl_args
204 }
204 }
205 local_tmpl_args.update(kwargs)
205 local_tmpl_args.update(kwargs)
206 return local_tmpl_args
206 return local_tmpl_args
207
207
208 def load_default_context(self):
208 def load_default_context(self):
209 """
209 """
210 example:
210 example:
211
211
212 def load_default_context(self):
212 def load_default_context(self):
213 c = self._get_local_tmpl_context()
213 c = self._get_local_tmpl_context()
214 c.custom_var = 'foobar'
214 c.custom_var = 'foobar'
215
215
216 return c
216 return c
217 """
217 """
218 raise NotImplementedError('Needs implementation in view class')
218 raise NotImplementedError('Needs implementation in view class')
219
219
220
220
221 class RepoAppView(BaseAppView):
221 class RepoAppView(BaseAppView):
222
222
223 def __init__(self, context, request):
223 def __init__(self, context, request):
224 super(RepoAppView, self).__init__(context, request)
224 super(RepoAppView, self).__init__(context, request)
225 self.db_repo = request.db_repo
225 self.db_repo = request.db_repo
226 self.db_repo_name = self.db_repo.repo_name
226 self.db_repo_name = self.db_repo.repo_name
227 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
227 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
228 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
228 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
229 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
229
230
230 def _handle_missing_requirements(self, error):
231 def _handle_missing_requirements(self, error):
231 log.error(
232 log.error(
232 'Requirements are missing for repository %s: %s',
233 'Requirements are missing for repository %s: %s',
233 self.db_repo_name, safe_unicode(error))
234 self.db_repo_name, safe_unicode(error))
234
235
235 def _get_local_tmpl_context(self, include_app_defaults=True):
236 def _get_local_tmpl_context(self, include_app_defaults=True):
236 _ = self.request.translate
237 _ = self.request.translate
237 c = super(RepoAppView, self)._get_local_tmpl_context(
238 c = super(RepoAppView, self)._get_local_tmpl_context(
238 include_app_defaults=include_app_defaults)
239 include_app_defaults=include_app_defaults)
239
240
240 # register common vars for this type of view
241 # register common vars for this type of view
241 c.rhodecode_db_repo = self.db_repo
242 c.rhodecode_db_repo = self.db_repo
242 c.repo_name = self.db_repo_name
243 c.repo_name = self.db_repo_name
243 c.repository_pull_requests = self.db_repo_pull_requests
244 c.repository_pull_requests = self.db_repo_pull_requests
244 c.repository_artifacts = self.db_repo_artifacts
245 c.repository_artifacts = self.db_repo_artifacts
245 c.repository_is_user_following = ScmModel().is_following_repo(
246 c.repository_is_user_following = ScmModel().is_following_repo(
246 self.db_repo_name, self._rhodecode_user.user_id)
247 self.db_repo_name, self._rhodecode_user.user_id)
247 self.path_filter = PathFilter(None)
248 self.path_filter = PathFilter(None)
248
249
249 c.repository_requirements_missing = {}
250 c.repository_requirements_missing = {}
250 try:
251 try:
251 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
252 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
252 # NOTE(marcink):
253 # NOTE(marcink):
253 # comparison to None since if it's an object __bool__ is expensive to
254 # comparison to None since if it's an object __bool__ is expensive to
254 # calculate
255 # calculate
255 if self.rhodecode_vcs_repo is not None:
256 if self.rhodecode_vcs_repo is not None:
256 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
257 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
257 c.auth_user.username)
258 c.auth_user.username)
258 self.path_filter = PathFilter(path_perms)
259 self.path_filter = PathFilter(path_perms)
259 except RepositoryRequirementError as e:
260 except RepositoryRequirementError as e:
260 c.repository_requirements_missing = {'error': str(e)}
261 c.repository_requirements_missing = {'error': str(e)}
261 self._handle_missing_requirements(e)
262 self._handle_missing_requirements(e)
262 self.rhodecode_vcs_repo = None
263 self.rhodecode_vcs_repo = None
263
264
264 c.path_filter = self.path_filter # used by atom_feed_entry.mako
265 c.path_filter = self.path_filter # used by atom_feed_entry.mako
265
266
266 if self.rhodecode_vcs_repo is None:
267 if self.rhodecode_vcs_repo is None:
267 # unable to fetch this repo as vcs instance, report back to user
268 # unable to fetch this repo as vcs instance, report back to user
268 h.flash(_(
269 h.flash(_(
269 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
270 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
270 "Please check if it exist, or is not damaged.") %
271 "Please check if it exist, or is not damaged.") %
271 {'repo_name': c.repo_name},
272 {'repo_name': c.repo_name},
272 category='error', ignore_duplicate=True)
273 category='error', ignore_duplicate=True)
273 if c.repository_requirements_missing:
274 if c.repository_requirements_missing:
274 route = self.request.matched_route.name
275 route = self.request.matched_route.name
275 if route.startswith(('edit_repo', 'repo_summary')):
276 if route.startswith(('edit_repo', 'repo_summary')):
276 # allow summary and edit repo on missing requirements
277 # allow summary and edit repo on missing requirements
277 return c
278 return c
278
279
279 raise HTTPFound(
280 raise HTTPFound(
280 h.route_path('repo_summary', repo_name=self.db_repo_name))
281 h.route_path('repo_summary', repo_name=self.db_repo_name))
281
282
282 else: # redirect if we don't show missing requirements
283 else: # redirect if we don't show missing requirements
283 raise HTTPFound(h.route_path('home'))
284 raise HTTPFound(h.route_path('home'))
284
285
285 c.has_origin_repo_read_perm = False
286 c.has_origin_repo_read_perm = False
286 if self.db_repo.fork:
287 if self.db_repo.fork:
287 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
288 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
288 'repository.write', 'repository.read', 'repository.admin')(
289 'repository.write', 'repository.read', 'repository.admin')(
289 self.db_repo.fork.repo_name, 'summary fork link')
290 self.db_repo.fork.repo_name, 'summary fork link')
290
291
291 return c
292 return c
292
293
293 def _get_f_path_unchecked(self, matchdict, default=None):
294 def _get_f_path_unchecked(self, matchdict, default=None):
294 """
295 """
295 Should only be used by redirects, everything else should call _get_f_path
296 Should only be used by redirects, everything else should call _get_f_path
296 """
297 """
297 f_path = matchdict.get('f_path')
298 f_path = matchdict.get('f_path')
298 if f_path:
299 if f_path:
299 # fix for multiple initial slashes that causes errors for GIT
300 # fix for multiple initial slashes that causes errors for GIT
300 return f_path.lstrip('/')
301 return f_path.lstrip('/')
301
302
302 return default
303 return default
303
304
304 def _get_f_path(self, matchdict, default=None):
305 def _get_f_path(self, matchdict, default=None):
305 f_path_match = self._get_f_path_unchecked(matchdict, default)
306 f_path_match = self._get_f_path_unchecked(matchdict, default)
306 return self.path_filter.assert_path_permissions(f_path_match)
307 return self.path_filter.assert_path_permissions(f_path_match)
307
308
308 def _get_general_setting(self, target_repo, settings_key, default=False):
309 def _get_general_setting(self, target_repo, settings_key, default=False):
309 settings_model = VcsSettingsModel(repo=target_repo)
310 settings_model = VcsSettingsModel(repo=target_repo)
310 settings = settings_model.get_general_settings()
311 settings = settings_model.get_general_settings()
311 return settings.get(settings_key, default)
312 return settings.get(settings_key, default)
312
313
313 def _get_repo_setting(self, target_repo, settings_key, default=False):
314 def _get_repo_setting(self, target_repo, settings_key, default=False):
314 settings_model = VcsSettingsModel(repo=target_repo)
315 settings_model = VcsSettingsModel(repo=target_repo)
315 settings = settings_model.get_repo_settings_inherited()
316 settings = settings_model.get_repo_settings_inherited()
316 return settings.get(settings_key, default)
317 return settings.get(settings_key, default)
317
318
318 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
319 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
319 log.debug('Looking for README file at path %s', path)
320 log.debug('Looking for README file at path %s', path)
320 if commit_id:
321 if commit_id:
321 landing_commit_id = commit_id
322 landing_commit_id = commit_id
322 else:
323 else:
323 landing_commit = db_repo.get_landing_commit()
324 landing_commit = db_repo.get_landing_commit()
324 if isinstance(landing_commit, EmptyCommit):
325 if isinstance(landing_commit, EmptyCommit):
325 return None, None
326 return None, None
326 landing_commit_id = landing_commit.raw_id
327 landing_commit_id = landing_commit.raw_id
327
328
328 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
329 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
329 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
330 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
330 start = time.time()
331 start = time.time()
331
332
332 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
333 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
333 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
334 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
334 readme_data = None
335 readme_data = None
335 readme_filename = None
336 readme_filename = None
336
337
337 commit = db_repo.get_commit(_commit_id)
338 commit = db_repo.get_commit(_commit_id)
338 log.debug("Searching for a README file at commit %s.", _commit_id)
339 log.debug("Searching for a README file at commit %s.", _commit_id)
339 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
340 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
340
341
341 if readme_node:
342 if readme_node:
342 log.debug('Found README node: %s', readme_node)
343 log.debug('Found README node: %s', readme_node)
343 relative_urls = {
344 relative_urls = {
344 'raw': h.route_path(
345 'raw': h.route_path(
345 'repo_file_raw', repo_name=_repo_name,
346 'repo_file_raw', repo_name=_repo_name,
346 commit_id=commit.raw_id, f_path=readme_node.path),
347 commit_id=commit.raw_id, f_path=readme_node.path),
347 'standard': h.route_path(
348 'standard': h.route_path(
348 'repo_files', repo_name=_repo_name,
349 'repo_files', repo_name=_repo_name,
349 commit_id=commit.raw_id, f_path=readme_node.path),
350 commit_id=commit.raw_id, f_path=readme_node.path),
350 }
351 }
351 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
352 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
352 readme_filename = readme_node.unicode_path
353 readme_filename = readme_node.unicode_path
353
354
354 return readme_data, readme_filename
355 return readme_data, readme_filename
355
356
356 readme_data, readme_filename = generate_repo_readme(
357 readme_data, readme_filename = generate_repo_readme(
357 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
358 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
358 compute_time = time.time() - start
359 compute_time = time.time() - start
359 log.debug('Repo README for path %s generated and computed in %.4fs',
360 log.debug('Repo README for path %s generated and computed in %.4fs',
360 path, compute_time)
361 path, compute_time)
361 return readme_data, readme_filename
362 return readme_data, readme_filename
362
363
363 def _render_readme_or_none(self, commit, readme_node, relative_urls):
364 def _render_readme_or_none(self, commit, readme_node, relative_urls):
364 log.debug('Found README file `%s` rendering...', readme_node.path)
365 log.debug('Found README file `%s` rendering...', readme_node.path)
365 renderer = MarkupRenderer()
366 renderer = MarkupRenderer()
366 try:
367 try:
367 html_source = renderer.render(
368 html_source = renderer.render(
368 readme_node.content, filename=readme_node.path)
369 readme_node.content, filename=readme_node.path)
369 if relative_urls:
370 if relative_urls:
370 return relative_links(html_source, relative_urls)
371 return relative_links(html_source, relative_urls)
371 return html_source
372 return html_source
372 except Exception:
373 except Exception:
373 log.exception(
374 log.exception(
374 "Exception while trying to render the README")
375 "Exception while trying to render the README")
375
376
376 def get_recache_flag(self):
377 def get_recache_flag(self):
377 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
378 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
378 flag_val = self.request.GET.get(flag_name)
379 flag_val = self.request.GET.get(flag_name)
379 if str2bool(flag_val):
380 if str2bool(flag_val):
380 return True
381 return True
381 return False
382 return False
382
383
383
384
384 class PathFilter(object):
385 class PathFilter(object):
385
386
386 # Expects and instance of BasePathPermissionChecker or None
387 # Expects and instance of BasePathPermissionChecker or None
387 def __init__(self, permission_checker):
388 def __init__(self, permission_checker):
388 self.permission_checker = permission_checker
389 self.permission_checker = permission_checker
389
390
390 def assert_path_permissions(self, path):
391 def assert_path_permissions(self, path):
391 if self.path_access_allowed(path):
392 if self.path_access_allowed(path):
392 return path
393 return path
393 raise HTTPForbidden()
394 raise HTTPForbidden()
394
395
395 def path_access_allowed(self, path):
396 def path_access_allowed(self, path):
396 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
397 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
397 if self.permission_checker:
398 if self.permission_checker:
398 return path and self.permission_checker.has_access(path)
399 return path and self.permission_checker.has_access(path)
399 return True
400 return True
400
401
401 def filter_patchset(self, patchset):
402 def filter_patchset(self, patchset):
402 if not self.permission_checker or not patchset:
403 if not self.permission_checker or not patchset:
403 return patchset, False
404 return patchset, False
404 had_filtered = False
405 had_filtered = False
405 filtered_patchset = []
406 filtered_patchset = []
406 for patch in patchset:
407 for patch in patchset:
407 filename = patch.get('filename', None)
408 filename = patch.get('filename', None)
408 if not filename or self.permission_checker.has_access(filename):
409 if not filename or self.permission_checker.has_access(filename):
409 filtered_patchset.append(patch)
410 filtered_patchset.append(patch)
410 else:
411 else:
411 had_filtered = True
412 had_filtered = True
412 if had_filtered:
413 if had_filtered:
413 if isinstance(patchset, diffs.LimitedDiffContainer):
414 if isinstance(patchset, diffs.LimitedDiffContainer):
414 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
415 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
415 return filtered_patchset, True
416 return filtered_patchset, True
416 else:
417 else:
417 return patchset, False
418 return patchset, False
418
419
419 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
420 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
420 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
421 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
421 result = diffset.render_patchset(
422 result = diffset.render_patchset(
422 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
423 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
423 result.has_hidden_changes = has_hidden_changes
424 result.has_hidden_changes = has_hidden_changes
424 return result
425 return result
425
426
426 def get_raw_patch(self, diff_processor):
427 def get_raw_patch(self, diff_processor):
427 if self.permission_checker is None:
428 if self.permission_checker is None:
428 return diff_processor.as_raw()
429 return diff_processor.as_raw()
429 elif self.permission_checker.has_full_access:
430 elif self.permission_checker.has_full_access:
430 return diff_processor.as_raw()
431 return diff_processor.as_raw()
431 else:
432 else:
432 return '# Repository has user-specific filters, raw patch generation is disabled.'
433 return '# Repository has user-specific filters, raw patch generation is disabled.'
433
434
434 @property
435 @property
435 def is_enabled(self):
436 def is_enabled(self):
436 return self.permission_checker is not None
437 return self.permission_checker is not None
437
438
438
439
439 class RepoGroupAppView(BaseAppView):
440 class RepoGroupAppView(BaseAppView):
440 def __init__(self, context, request):
441 def __init__(self, context, request):
441 super(RepoGroupAppView, self).__init__(context, request)
442 super(RepoGroupAppView, self).__init__(context, request)
442 self.db_repo_group = request.db_repo_group
443 self.db_repo_group = request.db_repo_group
443 self.db_repo_group_name = self.db_repo_group.group_name
444 self.db_repo_group_name = self.db_repo_group.group_name
444
445
445 def _get_local_tmpl_context(self, include_app_defaults=True):
446 def _get_local_tmpl_context(self, include_app_defaults=True):
446 _ = self.request.translate
447 _ = self.request.translate
447 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
448 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
448 include_app_defaults=include_app_defaults)
449 include_app_defaults=include_app_defaults)
449 c.repo_group = self.db_repo_group
450 c.repo_group = self.db_repo_group
450 return c
451 return c
451
452
452 def _revoke_perms_on_yourself(self, form_result):
453 def _revoke_perms_on_yourself(self, form_result):
453 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
454 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
454 form_result['perm_updates'])
455 form_result['perm_updates'])
455 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
456 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
456 form_result['perm_additions'])
457 form_result['perm_additions'])
457 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
458 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
458 form_result['perm_deletions'])
459 form_result['perm_deletions'])
459 admin_perm = 'group.admin'
460 admin_perm = 'group.admin'
460 if _updates and _updates[0][1] != admin_perm or \
461 if _updates and _updates[0][1] != admin_perm or \
461 _additions and _additions[0][1] != admin_perm or \
462 _additions and _additions[0][1] != admin_perm or \
462 _deletions and _deletions[0][1] != admin_perm:
463 _deletions and _deletions[0][1] != admin_perm:
463 return True
464 return True
464 return False
465 return False
465
466
466
467
467 class UserGroupAppView(BaseAppView):
468 class UserGroupAppView(BaseAppView):
468 def __init__(self, context, request):
469 def __init__(self, context, request):
469 super(UserGroupAppView, self).__init__(context, request)
470 super(UserGroupAppView, self).__init__(context, request)
470 self.db_user_group = request.db_user_group
471 self.db_user_group = request.db_user_group
471 self.db_user_group_name = self.db_user_group.users_group_name
472 self.db_user_group_name = self.db_user_group.users_group_name
472
473
473
474
474 class UserAppView(BaseAppView):
475 class UserAppView(BaseAppView):
475 def __init__(self, context, request):
476 def __init__(self, context, request):
476 super(UserAppView, self).__init__(context, request)
477 super(UserAppView, self).__init__(context, request)
477 self.db_user = request.db_user
478 self.db_user = request.db_user
478 self.db_user_id = self.db_user.user_id
479 self.db_user_id = self.db_user.user_id
479
480
480 _ = self.request.translate
481 _ = self.request.translate
481 if not request.db_user_supports_default:
482 if not request.db_user_supports_default:
482 if self.db_user.username == User.DEFAULT_USER:
483 if self.db_user.username == User.DEFAULT_USER:
483 h.flash(_("Editing user `{}` is disabled.".format(
484 h.flash(_("Editing user `{}` is disabled.".format(
484 User.DEFAULT_USER)), category='warning')
485 User.DEFAULT_USER)), category='warning')
485 raise HTTPFound(h.route_path('users'))
486 raise HTTPFound(h.route_path('users'))
486
487
487
488
488 class DataGridAppView(object):
489 class DataGridAppView(object):
489 """
490 """
490 Common class to have re-usable grid rendering components
491 Common class to have re-usable grid rendering components
491 """
492 """
492
493
493 def _extract_ordering(self, request, column_map=None):
494 def _extract_ordering(self, request, column_map=None):
494 column_map = column_map or {}
495 column_map = column_map or {}
495 column_index = safe_int(request.GET.get('order[0][column]'))
496 column_index = safe_int(request.GET.get('order[0][column]'))
496 order_dir = request.GET.get(
497 order_dir = request.GET.get(
497 'order[0][dir]', 'desc')
498 'order[0][dir]', 'desc')
498 order_by = request.GET.get(
499 order_by = request.GET.get(
499 'columns[%s][data][sort]' % column_index, 'name_raw')
500 'columns[%s][data][sort]' % column_index, 'name_raw')
500
501
501 # translate datatable to DB columns
502 # translate datatable to DB columns
502 order_by = column_map.get(order_by) or order_by
503 order_by = column_map.get(order_by) or order_by
503
504
504 search_q = request.GET.get('search[value]')
505 search_q = request.GET.get('search[value]')
505 return search_q, order_by, order_dir
506 return search_q, order_by, order_dir
506
507
507 def _extract_chunk(self, request):
508 def _extract_chunk(self, request):
508 start = safe_int(request.GET.get('start'), 0)
509 start = safe_int(request.GET.get('start'), 0)
509 length = safe_int(request.GET.get('length'), 25)
510 length = safe_int(request.GET.get('length'), 25)
510 draw = safe_int(request.GET.get('draw'))
511 draw = safe_int(request.GET.get('draw'))
511 return draw, start, length
512 return draw, start, length
512
513
513 def _get_order_col(self, order_by, model):
514 def _get_order_col(self, order_by, model):
514 if isinstance(order_by, compat.string_types):
515 if isinstance(order_by, compat.string_types):
515 try:
516 try:
516 return operator.attrgetter(order_by)(model)
517 return operator.attrgetter(order_by)(model)
517 except AttributeError:
518 except AttributeError:
518 return None
519 return None
519 else:
520 else:
520 return order_by
521 return order_by
521
522
522
523
523 class BaseReferencesView(RepoAppView):
524 class BaseReferencesView(RepoAppView):
524 """
525 """
525 Base for reference view for branches, tags and bookmarks.
526 Base for reference view for branches, tags and bookmarks.
526 """
527 """
527 def load_default_context(self):
528 def load_default_context(self):
528 c = self._get_local_tmpl_context()
529 c = self._get_local_tmpl_context()
529
530
530
531
531 return c
532 return c
532
533
533 def load_refs_context(self, ref_items, partials_template):
534 def load_refs_context(self, ref_items, partials_template):
534 _render = self.request.get_partial_renderer(partials_template)
535 _render = self.request.get_partial_renderer(partials_template)
535 pre_load = ["author", "date", "message", "parents"]
536 pre_load = ["author", "date", "message", "parents"]
536
537
537 is_svn = h.is_svn(self.rhodecode_vcs_repo)
538 is_svn = h.is_svn(self.rhodecode_vcs_repo)
538 is_hg = h.is_hg(self.rhodecode_vcs_repo)
539 is_hg = h.is_hg(self.rhodecode_vcs_repo)
539
540
540 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
541 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
541
542
542 closed_refs = {}
543 closed_refs = {}
543 if is_hg:
544 if is_hg:
544 closed_refs = self.rhodecode_vcs_repo.branches_closed
545 closed_refs = self.rhodecode_vcs_repo.branches_closed
545
546
546 data = []
547 data = []
547 for ref_name, commit_id in ref_items:
548 for ref_name, commit_id in ref_items:
548 commit = self.rhodecode_vcs_repo.get_commit(
549 commit = self.rhodecode_vcs_repo.get_commit(
549 commit_id=commit_id, pre_load=pre_load)
550 commit_id=commit_id, pre_load=pre_load)
550 closed = ref_name in closed_refs
551 closed = ref_name in closed_refs
551
552
552 # TODO: johbo: Unify generation of reference links
553 # TODO: johbo: Unify generation of reference links
553 use_commit_id = '/' in ref_name or is_svn
554 use_commit_id = '/' in ref_name or is_svn
554
555
555 if use_commit_id:
556 if use_commit_id:
556 files_url = h.route_path(
557 files_url = h.route_path(
557 'repo_files',
558 'repo_files',
558 repo_name=self.db_repo_name,
559 repo_name=self.db_repo_name,
559 f_path=ref_name if is_svn else '',
560 f_path=ref_name if is_svn else '',
560 commit_id=commit_id)
561 commit_id=commit_id)
561
562
562 else:
563 else:
563 files_url = h.route_path(
564 files_url = h.route_path(
564 'repo_files',
565 'repo_files',
565 repo_name=self.db_repo_name,
566 repo_name=self.db_repo_name,
566 f_path=ref_name if is_svn else '',
567 f_path=ref_name if is_svn else '',
567 commit_id=ref_name,
568 commit_id=ref_name,
568 _query=dict(at=ref_name))
569 _query=dict(at=ref_name))
569
570
570 data.append({
571 data.append({
571 "name": _render('name', ref_name, files_url, closed),
572 "name": _render('name', ref_name, files_url, closed),
572 "name_raw": ref_name,
573 "name_raw": ref_name,
573 "date": _render('date', commit.date),
574 "date": _render('date', commit.date),
574 "date_raw": datetime_to_time(commit.date),
575 "date_raw": datetime_to_time(commit.date),
575 "author": _render('author', commit.author),
576 "author": _render('author', commit.author),
576 "commit": _render(
577 "commit": _render(
577 'commit', commit.message, commit.raw_id, commit.idx),
578 'commit', commit.message, commit.raw_id, commit.idx),
578 "commit_raw": commit.idx,
579 "commit_raw": commit.idx,
579 "compare": _render(
580 "compare": _render(
580 'compare', format_ref_id(ref_name, commit.raw_id)),
581 'compare', format_ref_id(ref_name, commit.raw_id)),
581 })
582 })
582
583
583 return data
584 return data
584
585
585
586
586 class RepoRoutePredicate(object):
587 class RepoRoutePredicate(object):
587 def __init__(self, val, config):
588 def __init__(self, val, config):
588 self.val = val
589 self.val = val
589
590
590 def text(self):
591 def text(self):
591 return 'repo_route = %s' % self.val
592 return 'repo_route = %s' % self.val
592
593
593 phash = text
594 phash = text
594
595
595 def __call__(self, info, request):
596 def __call__(self, info, request):
596 if hasattr(request, 'vcs_call'):
597 if hasattr(request, 'vcs_call'):
597 # skip vcs calls
598 # skip vcs calls
598 return
599 return
599
600
600 repo_name = info['match']['repo_name']
601 repo_name = info['match']['repo_name']
601 repo_model = repo.RepoModel()
602 repo_model = repo.RepoModel()
602
603
603 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
604 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
604
605
605 def redirect_if_creating(route_info, db_repo):
606 def redirect_if_creating(route_info, db_repo):
606 skip_views = ['edit_repo_advanced_delete']
607 skip_views = ['edit_repo_advanced_delete']
607 route = route_info['route']
608 route = route_info['route']
608 # we should skip delete view so we can actually "remove" repositories
609 # we should skip delete view so we can actually "remove" repositories
609 # if they get stuck in creating state.
610 # if they get stuck in creating state.
610 if route.name in skip_views:
611 if route.name in skip_views:
611 return
612 return
612
613
613 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
614 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
614 repo_creating_url = request.route_path(
615 repo_creating_url = request.route_path(
615 'repo_creating', repo_name=db_repo.repo_name)
616 'repo_creating', repo_name=db_repo.repo_name)
616 raise HTTPFound(repo_creating_url)
617 raise HTTPFound(repo_creating_url)
617
618
618 if by_name_match:
619 if by_name_match:
619 # register this as request object we can re-use later
620 # register this as request object we can re-use later
620 request.db_repo = by_name_match
621 request.db_repo = by_name_match
621 redirect_if_creating(info, by_name_match)
622 redirect_if_creating(info, by_name_match)
622 return True
623 return True
623
624
624 by_id_match = repo_model.get_repo_by_id(repo_name)
625 by_id_match = repo_model.get_repo_by_id(repo_name)
625 if by_id_match:
626 if by_id_match:
626 request.db_repo = by_id_match
627 request.db_repo = by_id_match
627 redirect_if_creating(info, by_id_match)
628 redirect_if_creating(info, by_id_match)
628 return True
629 return True
629
630
630 return False
631 return False
631
632
632
633
633 class RepoForbidArchivedRoutePredicate(object):
634 class RepoForbidArchivedRoutePredicate(object):
634 def __init__(self, val, config):
635 def __init__(self, val, config):
635 self.val = val
636 self.val = val
636
637
637 def text(self):
638 def text(self):
638 return 'repo_forbid_archived = %s' % self.val
639 return 'repo_forbid_archived = %s' % self.val
639
640
640 phash = text
641 phash = text
641
642
642 def __call__(self, info, request):
643 def __call__(self, info, request):
643 _ = request.translate
644 _ = request.translate
644 rhodecode_db_repo = request.db_repo
645 rhodecode_db_repo = request.db_repo
645
646
646 log.debug(
647 log.debug(
647 '%s checking if archived flag for repo for %s',
648 '%s checking if archived flag for repo for %s',
648 self.__class__.__name__, rhodecode_db_repo.repo_name)
649 self.__class__.__name__, rhodecode_db_repo.repo_name)
649
650
650 if rhodecode_db_repo.archived:
651 if rhodecode_db_repo.archived:
651 log.warning('Current view is not supported for archived repo:%s',
652 log.warning('Current view is not supported for archived repo:%s',
652 rhodecode_db_repo.repo_name)
653 rhodecode_db_repo.repo_name)
653
654
654 h.flash(
655 h.flash(
655 h.literal(_('Action not supported for archived repository.')),
656 h.literal(_('Action not supported for archived repository.')),
656 category='warning')
657 category='warning')
657 summary_url = request.route_path(
658 summary_url = request.route_path(
658 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
659 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
659 raise HTTPFound(summary_url)
660 raise HTTPFound(summary_url)
660 return True
661 return True
661
662
662
663
663 class RepoTypeRoutePredicate(object):
664 class RepoTypeRoutePredicate(object):
664 def __init__(self, val, config):
665 def __init__(self, val, config):
665 self.val = val or ['hg', 'git', 'svn']
666 self.val = val or ['hg', 'git', 'svn']
666
667
667 def text(self):
668 def text(self):
668 return 'repo_accepted_type = %s' % self.val
669 return 'repo_accepted_type = %s' % self.val
669
670
670 phash = text
671 phash = text
671
672
672 def __call__(self, info, request):
673 def __call__(self, info, request):
673 if hasattr(request, 'vcs_call'):
674 if hasattr(request, 'vcs_call'):
674 # skip vcs calls
675 # skip vcs calls
675 return
676 return
676
677
677 rhodecode_db_repo = request.db_repo
678 rhodecode_db_repo = request.db_repo
678
679
679 log.debug(
680 log.debug(
680 '%s checking repo type for %s in %s',
681 '%s checking repo type for %s in %s',
681 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
682 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
682
683
683 if rhodecode_db_repo.repo_type in self.val:
684 if rhodecode_db_repo.repo_type in self.val:
684 return True
685 return True
685 else:
686 else:
686 log.warning('Current view is not supported for repo type:%s',
687 log.warning('Current view is not supported for repo type:%s',
687 rhodecode_db_repo.repo_type)
688 rhodecode_db_repo.repo_type)
688 return False
689 return False
689
690
690
691
691 class RepoGroupRoutePredicate(object):
692 class RepoGroupRoutePredicate(object):
692 def __init__(self, val, config):
693 def __init__(self, val, config):
693 self.val = val
694 self.val = val
694
695
695 def text(self):
696 def text(self):
696 return 'repo_group_route = %s' % self.val
697 return 'repo_group_route = %s' % self.val
697
698
698 phash = text
699 phash = text
699
700
700 def __call__(self, info, request):
701 def __call__(self, info, request):
701 if hasattr(request, 'vcs_call'):
702 if hasattr(request, 'vcs_call'):
702 # skip vcs calls
703 # skip vcs calls
703 return
704 return
704
705
705 repo_group_name = info['match']['repo_group_name']
706 repo_group_name = info['match']['repo_group_name']
706 repo_group_model = repo_group.RepoGroupModel()
707 repo_group_model = repo_group.RepoGroupModel()
707 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
708 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
708
709
709 if by_name_match:
710 if by_name_match:
710 # register this as request object we can re-use later
711 # register this as request object we can re-use later
711 request.db_repo_group = by_name_match
712 request.db_repo_group = by_name_match
712 return True
713 return True
713
714
714 return False
715 return False
715
716
716
717
717 class UserGroupRoutePredicate(object):
718 class UserGroupRoutePredicate(object):
718 def __init__(self, val, config):
719 def __init__(self, val, config):
719 self.val = val
720 self.val = val
720
721
721 def text(self):
722 def text(self):
722 return 'user_group_route = %s' % self.val
723 return 'user_group_route = %s' % self.val
723
724
724 phash = text
725 phash = text
725
726
726 def __call__(self, info, request):
727 def __call__(self, info, request):
727 if hasattr(request, 'vcs_call'):
728 if hasattr(request, 'vcs_call'):
728 # skip vcs calls
729 # skip vcs calls
729 return
730 return
730
731
731 user_group_id = info['match']['user_group_id']
732 user_group_id = info['match']['user_group_id']
732 user_group_model = user_group.UserGroup()
733 user_group_model = user_group.UserGroup()
733 by_id_match = user_group_model.get(user_group_id, cache=False)
734 by_id_match = user_group_model.get(user_group_id, cache=False)
734
735
735 if by_id_match:
736 if by_id_match:
736 # register this as request object we can re-use later
737 # register this as request object we can re-use later
737 request.db_user_group = by_id_match
738 request.db_user_group = by_id_match
738 return True
739 return True
739
740
740 return False
741 return False
741
742
742
743
743 class UserRoutePredicateBase(object):
744 class UserRoutePredicateBase(object):
744 supports_default = None
745 supports_default = None
745
746
746 def __init__(self, val, config):
747 def __init__(self, val, config):
747 self.val = val
748 self.val = val
748
749
749 def text(self):
750 def text(self):
750 raise NotImplementedError()
751 raise NotImplementedError()
751
752
752 def __call__(self, info, request):
753 def __call__(self, info, request):
753 if hasattr(request, 'vcs_call'):
754 if hasattr(request, 'vcs_call'):
754 # skip vcs calls
755 # skip vcs calls
755 return
756 return
756
757
757 user_id = info['match']['user_id']
758 user_id = info['match']['user_id']
758 user_model = user.User()
759 user_model = user.User()
759 by_id_match = user_model.get(user_id, cache=False)
760 by_id_match = user_model.get(user_id, cache=False)
760
761
761 if by_id_match:
762 if by_id_match:
762 # register this as request object we can re-use later
763 # register this as request object we can re-use later
763 request.db_user = by_id_match
764 request.db_user = by_id_match
764 request.db_user_supports_default = self.supports_default
765 request.db_user_supports_default = self.supports_default
765 return True
766 return True
766
767
767 return False
768 return False
768
769
769
770
770 class UserRoutePredicate(UserRoutePredicateBase):
771 class UserRoutePredicate(UserRoutePredicateBase):
771 supports_default = False
772 supports_default = False
772
773
773 def text(self):
774 def text(self):
774 return 'user_route = %s' % self.val
775 return 'user_route = %s' % self.val
775
776
776 phash = text
777 phash = text
777
778
778
779
779 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
780 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
780 supports_default = True
781 supports_default = True
781
782
782 def text(self):
783 def text(self):
783 return 'user_with_default_route = %s' % self.val
784 return 'user_with_default_route = %s' % self.val
784
785
785 phash = text
786 phash = text
786
787
787
788
788 def includeme(config):
789 def includeme(config):
789 config.add_route_predicate(
790 config.add_route_predicate(
790 'repo_route', RepoRoutePredicate)
791 'repo_route', RepoRoutePredicate)
791 config.add_route_predicate(
792 config.add_route_predicate(
792 'repo_accepted_types', RepoTypeRoutePredicate)
793 'repo_accepted_types', RepoTypeRoutePredicate)
793 config.add_route_predicate(
794 config.add_route_predicate(
794 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
795 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
795 config.add_route_predicate(
796 config.add_route_predicate(
796 'repo_group_route', RepoGroupRoutePredicate)
797 'repo_group_route', RepoGroupRoutePredicate)
797 config.add_route_predicate(
798 config.add_route_predicate(
798 'user_group_route', UserGroupRoutePredicate)
799 'user_group_route', UserGroupRoutePredicate)
799 config.add_route_predicate(
800 config.add_route_predicate(
800 'user_route_with_default', UserRouteWithDefaultPredicate)
801 'user_route_with_default', UserRouteWithDefaultPredicate)
801 config.add_route_predicate(
802 config.add_route_predicate(
802 'user_route', UserRoutePredicate)
803 'user_route', UserRoutePredicate)
@@ -1,782 +1,782 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 import datetime
25 import datetime
26 import formencode
26 import formencode
27 import formencode.htmlfill
27 import formencode.htmlfill
28
28
29 import rhodecode
29 import rhodecode
30 from pyramid.view import view_config
30 from pyramid.view import view_config
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34
34
35 from rhodecode.apps._base import BaseAppView
35 from rhodecode.apps._base import BaseAppView
36 from rhodecode.apps._base.navigation import navigation_list
36 from rhodecode.apps._base.navigation import navigation_list
37 from rhodecode.apps.svn_support.config_keys import generate_config
37 from rhodecode.apps.svn_support.config_keys import generate_config
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.celerylib import tasks, run_task
42 from rhodecode.lib.utils import repo2db_mapper
42 from rhodecode.lib.utils import repo2db_mapper
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 from rhodecode.lib.index import searcher_from_config
44 from rhodecode.lib.index import searcher_from_config
45
45
46 from rhodecode.model.db import RhodeCodeUi, Repository
46 from rhodecode.model.db import RhodeCodeUi, Repository
47 from rhodecode.model.forms import (ApplicationSettingsForm,
47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 LabsSettingsForm, IssueTrackerPatternsForm)
49 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
51
51
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.notification import EmailNotificationModel
53 from rhodecode.model.notification import EmailNotificationModel
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.settings import (
55 from rhodecode.model.settings import (
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 SettingsModel)
57 SettingsModel)
58
58
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class AdminSettingsView(BaseAppView):
63 class AdminSettingsView(BaseAppView):
64
64
65 def load_default_context(self):
65 def load_default_context(self):
66 c = self._get_local_tmpl_context()
66 c = self._get_local_tmpl_context()
67 c.labs_active = str2bool(
67 c.labs_active = str2bool(
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 c.navlist = navigation_list(self.request)
69 c.navlist = navigation_list(self.request)
70
70
71 return c
71 return c
72
72
73 @classmethod
73 @classmethod
74 def _get_ui_settings(cls):
74 def _get_ui_settings(cls):
75 ret = RhodeCodeUi.query().all()
75 ret = RhodeCodeUi.query().all()
76
76
77 if not ret:
77 if not ret:
78 raise Exception('Could not get application ui settings !')
78 raise Exception('Could not get application ui settings !')
79 settings = {}
79 settings = {}
80 for each in ret:
80 for each in ret:
81 k = each.ui_key
81 k = each.ui_key
82 v = each.ui_value
82 v = each.ui_value
83 if k == '/':
83 if k == '/':
84 k = 'root_path'
84 k = 'root_path'
85
85
86 if k in ['push_ssl', 'publish', 'enabled']:
86 if k in ['push_ssl', 'publish', 'enabled']:
87 v = str2bool(v)
87 v = str2bool(v)
88
88
89 if k.find('.') != -1:
89 if k.find('.') != -1:
90 k = k.replace('.', '_')
90 k = k.replace('.', '_')
91
91
92 if each.ui_section in ['hooks', 'extensions']:
92 if each.ui_section in ['hooks', 'extensions']:
93 v = each.ui_active
93 v = each.ui_active
94
94
95 settings[each.ui_section + '_' + k] = v
95 settings[each.ui_section + '_' + k] = v
96 return settings
96 return settings
97
97
98 @classmethod
98 @classmethod
99 def _form_defaults(cls):
99 def _form_defaults(cls):
100 defaults = SettingsModel().get_all_settings()
100 defaults = SettingsModel().get_all_settings()
101 defaults.update(cls._get_ui_settings())
101 defaults.update(cls._get_ui_settings())
102
102
103 defaults.update({
103 defaults.update({
104 'new_svn_branch': '',
104 'new_svn_branch': '',
105 'new_svn_tag': '',
105 'new_svn_tag': '',
106 })
106 })
107 return defaults
107 return defaults
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasPermissionAllDecorator('hg.admin')
110 @HasPermissionAllDecorator('hg.admin')
111 @view_config(
111 @view_config(
112 route_name='admin_settings_vcs', request_method='GET',
112 route_name='admin_settings_vcs', request_method='GET',
113 renderer='rhodecode:templates/admin/settings/settings.mako')
113 renderer='rhodecode:templates/admin/settings/settings.mako')
114 def settings_vcs(self):
114 def settings_vcs(self):
115 c = self.load_default_context()
115 c = self.load_default_context()
116 c.active = 'vcs'
116 c.active = 'vcs'
117 model = VcsSettingsModel()
117 model = VcsSettingsModel()
118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120
120
121 settings = self.request.registry.settings
121 settings = self.request.registry.settings
122 c.svn_proxy_generate_config = settings[generate_config]
122 c.svn_proxy_generate_config = settings[generate_config]
123
123
124 defaults = self._form_defaults()
124 defaults = self._form_defaults()
125
125
126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127
127
128 data = render('rhodecode:templates/admin/settings/settings.mako',
128 data = render('rhodecode:templates/admin/settings/settings.mako',
129 self._get_template_context(c), self.request)
129 self._get_template_context(c), self.request)
130 html = formencode.htmlfill.render(
130 html = formencode.htmlfill.render(
131 data,
131 data,
132 defaults=defaults,
132 defaults=defaults,
133 encoding="UTF-8",
133 encoding="UTF-8",
134 force_defaults=False
134 force_defaults=False
135 )
135 )
136 return Response(html)
136 return Response(html)
137
137
138 @LoginRequired()
138 @LoginRequired()
139 @HasPermissionAllDecorator('hg.admin')
139 @HasPermissionAllDecorator('hg.admin')
140 @CSRFRequired()
140 @CSRFRequired()
141 @view_config(
141 @view_config(
142 route_name='admin_settings_vcs_update', request_method='POST',
142 route_name='admin_settings_vcs_update', request_method='POST',
143 renderer='rhodecode:templates/admin/settings/settings.mako')
143 renderer='rhodecode:templates/admin/settings/settings.mako')
144 def settings_vcs_update(self):
144 def settings_vcs_update(self):
145 _ = self.request.translate
145 _ = self.request.translate
146 c = self.load_default_context()
146 c = self.load_default_context()
147 c.active = 'vcs'
147 c.active = 'vcs'
148
148
149 model = VcsSettingsModel()
149 model = VcsSettingsModel()
150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152
152
153 settings = self.request.registry.settings
153 settings = self.request.registry.settings
154 c.svn_proxy_generate_config = settings[generate_config]
154 c.svn_proxy_generate_config = settings[generate_config]
155
155
156 application_form = ApplicationUiSettingsForm(self.request.translate)()
156 application_form = ApplicationUiSettingsForm(self.request.translate)()
157
157
158 try:
158 try:
159 form_result = application_form.to_python(dict(self.request.POST))
159 form_result = application_form.to_python(dict(self.request.POST))
160 except formencode.Invalid as errors:
160 except formencode.Invalid as errors:
161 h.flash(
161 h.flash(
162 _("Some form inputs contain invalid data."),
162 _("Some form inputs contain invalid data."),
163 category='error')
163 category='error')
164 data = render('rhodecode:templates/admin/settings/settings.mako',
164 data = render('rhodecode:templates/admin/settings/settings.mako',
165 self._get_template_context(c), self.request)
165 self._get_template_context(c), self.request)
166 html = formencode.htmlfill.render(
166 html = formencode.htmlfill.render(
167 data,
167 data,
168 defaults=errors.value,
168 defaults=errors.value,
169 errors=errors.error_dict or {},
169 errors=errors.error_dict or {},
170 prefix_error=False,
170 prefix_error=False,
171 encoding="UTF-8",
171 encoding="UTF-8",
172 force_defaults=False
172 force_defaults=False
173 )
173 )
174 return Response(html)
174 return Response(html)
175
175
176 try:
176 try:
177 if c.visual.allow_repo_location_change:
177 if c.visual.allow_repo_location_change:
178 model.update_global_path_setting(form_result['paths_root_path'])
178 model.update_global_path_setting(form_result['paths_root_path'])
179
179
180 model.update_global_ssl_setting(form_result['web_push_ssl'])
180 model.update_global_ssl_setting(form_result['web_push_ssl'])
181 model.update_global_hook_settings(form_result)
181 model.update_global_hook_settings(form_result)
182
182
183 model.create_or_update_global_svn_settings(form_result)
183 model.create_or_update_global_svn_settings(form_result)
184 model.create_or_update_global_hg_settings(form_result)
184 model.create_or_update_global_hg_settings(form_result)
185 model.create_or_update_global_git_settings(form_result)
185 model.create_or_update_global_git_settings(form_result)
186 model.create_or_update_global_pr_settings(form_result)
186 model.create_or_update_global_pr_settings(form_result)
187 except Exception:
187 except Exception:
188 log.exception("Exception while updating settings")
188 log.exception("Exception while updating settings")
189 h.flash(_('Error occurred during updating '
189 h.flash(_('Error occurred during updating '
190 'application settings'), category='error')
190 'application settings'), category='error')
191 else:
191 else:
192 Session().commit()
192 Session().commit()
193 h.flash(_('Updated VCS settings'), category='success')
193 h.flash(_('Updated VCS settings'), category='success')
194 raise HTTPFound(h.route_path('admin_settings_vcs'))
194 raise HTTPFound(h.route_path('admin_settings_vcs'))
195
195
196 data = render('rhodecode:templates/admin/settings/settings.mako',
196 data = render('rhodecode:templates/admin/settings/settings.mako',
197 self._get_template_context(c), self.request)
197 self._get_template_context(c), self.request)
198 html = formencode.htmlfill.render(
198 html = formencode.htmlfill.render(
199 data,
199 data,
200 defaults=self._form_defaults(),
200 defaults=self._form_defaults(),
201 encoding="UTF-8",
201 encoding="UTF-8",
202 force_defaults=False
202 force_defaults=False
203 )
203 )
204 return Response(html)
204 return Response(html)
205
205
206 @LoginRequired()
206 @LoginRequired()
207 @HasPermissionAllDecorator('hg.admin')
207 @HasPermissionAllDecorator('hg.admin')
208 @CSRFRequired()
208 @CSRFRequired()
209 @view_config(
209 @view_config(
210 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
210 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
211 renderer='json_ext', xhr=True)
211 renderer='json_ext', xhr=True)
212 def settings_vcs_delete_svn_pattern(self):
212 def settings_vcs_delete_svn_pattern(self):
213 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
213 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
214 model = VcsSettingsModel()
214 model = VcsSettingsModel()
215 try:
215 try:
216 model.delete_global_svn_pattern(delete_pattern_id)
216 model.delete_global_svn_pattern(delete_pattern_id)
217 except SettingNotFound:
217 except SettingNotFound:
218 log.exception(
218 log.exception(
219 'Failed to delete svn_pattern with id %s', delete_pattern_id)
219 'Failed to delete svn_pattern with id %s', delete_pattern_id)
220 raise HTTPNotFound()
220 raise HTTPNotFound()
221
221
222 Session().commit()
222 Session().commit()
223 return True
223 return True
224
224
225 @LoginRequired()
225 @LoginRequired()
226 @HasPermissionAllDecorator('hg.admin')
226 @HasPermissionAllDecorator('hg.admin')
227 @view_config(
227 @view_config(
228 route_name='admin_settings_mapping', request_method='GET',
228 route_name='admin_settings_mapping', request_method='GET',
229 renderer='rhodecode:templates/admin/settings/settings.mako')
229 renderer='rhodecode:templates/admin/settings/settings.mako')
230 def settings_mapping(self):
230 def settings_mapping(self):
231 c = self.load_default_context()
231 c = self.load_default_context()
232 c.active = 'mapping'
232 c.active = 'mapping'
233
233
234 data = render('rhodecode:templates/admin/settings/settings.mako',
234 data = render('rhodecode:templates/admin/settings/settings.mako',
235 self._get_template_context(c), self.request)
235 self._get_template_context(c), self.request)
236 html = formencode.htmlfill.render(
236 html = formencode.htmlfill.render(
237 data,
237 data,
238 defaults=self._form_defaults(),
238 defaults=self._form_defaults(),
239 encoding="UTF-8",
239 encoding="UTF-8",
240 force_defaults=False
240 force_defaults=False
241 )
241 )
242 return Response(html)
242 return Response(html)
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @HasPermissionAllDecorator('hg.admin')
245 @HasPermissionAllDecorator('hg.admin')
246 @CSRFRequired()
246 @CSRFRequired()
247 @view_config(
247 @view_config(
248 route_name='admin_settings_mapping_update', request_method='POST',
248 route_name='admin_settings_mapping_update', request_method='POST',
249 renderer='rhodecode:templates/admin/settings/settings.mako')
249 renderer='rhodecode:templates/admin/settings/settings.mako')
250 def settings_mapping_update(self):
250 def settings_mapping_update(self):
251 _ = self.request.translate
251 _ = self.request.translate
252 c = self.load_default_context()
252 c = self.load_default_context()
253 c.active = 'mapping'
253 c.active = 'mapping'
254 rm_obsolete = self.request.POST.get('destroy', False)
254 rm_obsolete = self.request.POST.get('destroy', False)
255 invalidate_cache = self.request.POST.get('invalidate', False)
255 invalidate_cache = self.request.POST.get('invalidate', False)
256 log.debug(
256 log.debug(
257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
258
258
259 if invalidate_cache:
259 if invalidate_cache:
260 log.debug('invalidating all repositories cache')
260 log.debug('invalidating all repositories cache')
261 for repo in Repository.get_all():
261 for repo in Repository.get_all():
262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
263
263
264 filesystem_repos = ScmModel().repo_scan()
264 filesystem_repos = ScmModel().repo_scan()
265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
266 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
267 h.flash(_('Repositories successfully '
267 h.flash(_('Repositories successfully '
268 'rescanned added: %s ; removed: %s') %
268 'rescanned added: %s ; removed: %s') %
269 (_repr(added), _repr(removed)),
269 (_repr(added), _repr(removed)),
270 category='success')
270 category='success')
271 raise HTTPFound(h.route_path('admin_settings_mapping'))
271 raise HTTPFound(h.route_path('admin_settings_mapping'))
272
272
273 @LoginRequired()
273 @LoginRequired()
274 @HasPermissionAllDecorator('hg.admin')
274 @HasPermissionAllDecorator('hg.admin')
275 @view_config(
275 @view_config(
276 route_name='admin_settings', request_method='GET',
276 route_name='admin_settings', request_method='GET',
277 renderer='rhodecode:templates/admin/settings/settings.mako')
277 renderer='rhodecode:templates/admin/settings/settings.mako')
278 @view_config(
278 @view_config(
279 route_name='admin_settings_global', request_method='GET',
279 route_name='admin_settings_global', request_method='GET',
280 renderer='rhodecode:templates/admin/settings/settings.mako')
280 renderer='rhodecode:templates/admin/settings/settings.mako')
281 def settings_global(self):
281 def settings_global(self):
282 c = self.load_default_context()
282 c = self.load_default_context()
283 c.active = 'global'
283 c.active = 'global'
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 .get_personal_group_name_pattern()
285 .get_personal_group_name_pattern()
286
286
287 data = render('rhodecode:templates/admin/settings/settings.mako',
287 data = render('rhodecode:templates/admin/settings/settings.mako',
288 self._get_template_context(c), self.request)
288 self._get_template_context(c), self.request)
289 html = formencode.htmlfill.render(
289 html = formencode.htmlfill.render(
290 data,
290 data,
291 defaults=self._form_defaults(),
291 defaults=self._form_defaults(),
292 encoding="UTF-8",
292 encoding="UTF-8",
293 force_defaults=False
293 force_defaults=False
294 )
294 )
295 return Response(html)
295 return Response(html)
296
296
297 @LoginRequired()
297 @LoginRequired()
298 @HasPermissionAllDecorator('hg.admin')
298 @HasPermissionAllDecorator('hg.admin')
299 @CSRFRequired()
299 @CSRFRequired()
300 @view_config(
300 @view_config(
301 route_name='admin_settings_update', request_method='POST',
301 route_name='admin_settings_update', request_method='POST',
302 renderer='rhodecode:templates/admin/settings/settings.mako')
302 renderer='rhodecode:templates/admin/settings/settings.mako')
303 @view_config(
303 @view_config(
304 route_name='admin_settings_global_update', request_method='POST',
304 route_name='admin_settings_global_update', request_method='POST',
305 renderer='rhodecode:templates/admin/settings/settings.mako')
305 renderer='rhodecode:templates/admin/settings/settings.mako')
306 def settings_global_update(self):
306 def settings_global_update(self):
307 _ = self.request.translate
307 _ = self.request.translate
308 c = self.load_default_context()
308 c = self.load_default_context()
309 c.active = 'global'
309 c.active = 'global'
310 c.personal_repo_group_default_pattern = RepoGroupModel()\
310 c.personal_repo_group_default_pattern = RepoGroupModel()\
311 .get_personal_group_name_pattern()
311 .get_personal_group_name_pattern()
312 application_form = ApplicationSettingsForm(self.request.translate)()
312 application_form = ApplicationSettingsForm(self.request.translate)()
313 try:
313 try:
314 form_result = application_form.to_python(dict(self.request.POST))
314 form_result = application_form.to_python(dict(self.request.POST))
315 except formencode.Invalid as errors:
315 except formencode.Invalid as errors:
316 h.flash(
316 h.flash(
317 _("Some form inputs contain invalid data."),
317 _("Some form inputs contain invalid data."),
318 category='error')
318 category='error')
319 data = render('rhodecode:templates/admin/settings/settings.mako',
319 data = render('rhodecode:templates/admin/settings/settings.mako',
320 self._get_template_context(c), self.request)
320 self._get_template_context(c), self.request)
321 html = formencode.htmlfill.render(
321 html = formencode.htmlfill.render(
322 data,
322 data,
323 defaults=errors.value,
323 defaults=errors.value,
324 errors=errors.error_dict or {},
324 errors=errors.error_dict or {},
325 prefix_error=False,
325 prefix_error=False,
326 encoding="UTF-8",
326 encoding="UTF-8",
327 force_defaults=False
327 force_defaults=False
328 )
328 )
329 return Response(html)
329 return Response(html)
330
330
331 settings = [
331 settings = [
332 ('title', 'rhodecode_title', 'unicode'),
332 ('title', 'rhodecode_title', 'unicode'),
333 ('realm', 'rhodecode_realm', 'unicode'),
333 ('realm', 'rhodecode_realm', 'unicode'),
334 ('pre_code', 'rhodecode_pre_code', 'unicode'),
334 ('pre_code', 'rhodecode_pre_code', 'unicode'),
335 ('post_code', 'rhodecode_post_code', 'unicode'),
335 ('post_code', 'rhodecode_post_code', 'unicode'),
336 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
336 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
337 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
337 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
338 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
338 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
339 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
339 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
340 ]
340 ]
341 try:
341 try:
342 for setting, form_key, type_ in settings:
342 for setting, form_key, type_ in settings:
343 sett = SettingsModel().create_or_update_setting(
343 sett = SettingsModel().create_or_update_setting(
344 setting, form_result[form_key], type_)
344 setting, form_result[form_key], type_)
345 Session().add(sett)
345 Session().add(sett)
346
346
347 Session().commit()
347 Session().commit()
348 SettingsModel().invalidate_settings_cache()
348 SettingsModel().invalidate_settings_cache()
349 h.flash(_('Updated application settings'), category='success')
349 h.flash(_('Updated application settings'), category='success')
350 except Exception:
350 except Exception:
351 log.exception("Exception while updating application settings")
351 log.exception("Exception while updating application settings")
352 h.flash(
352 h.flash(
353 _('Error occurred during updating application settings'),
353 _('Error occurred during updating application settings'),
354 category='error')
354 category='error')
355
355
356 raise HTTPFound(h.route_path('admin_settings_global'))
356 raise HTTPFound(h.route_path('admin_settings_global'))
357
357
358 @LoginRequired()
358 @LoginRequired()
359 @HasPermissionAllDecorator('hg.admin')
359 @HasPermissionAllDecorator('hg.admin')
360 @view_config(
360 @view_config(
361 route_name='admin_settings_visual', request_method='GET',
361 route_name='admin_settings_visual', request_method='GET',
362 renderer='rhodecode:templates/admin/settings/settings.mako')
362 renderer='rhodecode:templates/admin/settings/settings.mako')
363 def settings_visual(self):
363 def settings_visual(self):
364 c = self.load_default_context()
364 c = self.load_default_context()
365 c.active = 'visual'
365 c.active = 'visual'
366
366
367 data = render('rhodecode:templates/admin/settings/settings.mako',
367 data = render('rhodecode:templates/admin/settings/settings.mako',
368 self._get_template_context(c), self.request)
368 self._get_template_context(c), self.request)
369 html = formencode.htmlfill.render(
369 html = formencode.htmlfill.render(
370 data,
370 data,
371 defaults=self._form_defaults(),
371 defaults=self._form_defaults(),
372 encoding="UTF-8",
372 encoding="UTF-8",
373 force_defaults=False
373 force_defaults=False
374 )
374 )
375 return Response(html)
375 return Response(html)
376
376
377 @LoginRequired()
377 @LoginRequired()
378 @HasPermissionAllDecorator('hg.admin')
378 @HasPermissionAllDecorator('hg.admin')
379 @CSRFRequired()
379 @CSRFRequired()
380 @view_config(
380 @view_config(
381 route_name='admin_settings_visual_update', request_method='POST',
381 route_name='admin_settings_visual_update', request_method='POST',
382 renderer='rhodecode:templates/admin/settings/settings.mako')
382 renderer='rhodecode:templates/admin/settings/settings.mako')
383 def settings_visual_update(self):
383 def settings_visual_update(self):
384 _ = self.request.translate
384 _ = self.request.translate
385 c = self.load_default_context()
385 c = self.load_default_context()
386 c.active = 'visual'
386 c.active = 'visual'
387 application_form = ApplicationVisualisationForm(self.request.translate)()
387 application_form = ApplicationVisualisationForm(self.request.translate)()
388 try:
388 try:
389 form_result = application_form.to_python(dict(self.request.POST))
389 form_result = application_form.to_python(dict(self.request.POST))
390 except formencode.Invalid as errors:
390 except formencode.Invalid as errors:
391 h.flash(
391 h.flash(
392 _("Some form inputs contain invalid data."),
392 _("Some form inputs contain invalid data."),
393 category='error')
393 category='error')
394 data = render('rhodecode:templates/admin/settings/settings.mako',
394 data = render('rhodecode:templates/admin/settings/settings.mako',
395 self._get_template_context(c), self.request)
395 self._get_template_context(c), self.request)
396 html = formencode.htmlfill.render(
396 html = formencode.htmlfill.render(
397 data,
397 data,
398 defaults=errors.value,
398 defaults=errors.value,
399 errors=errors.error_dict or {},
399 errors=errors.error_dict or {},
400 prefix_error=False,
400 prefix_error=False,
401 encoding="UTF-8",
401 encoding="UTF-8",
402 force_defaults=False
402 force_defaults=False
403 )
403 )
404 return Response(html)
404 return Response(html)
405
405
406 try:
406 try:
407 settings = [
407 settings = [
408 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
408 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
409 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
409 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
410 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
410 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
411 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
411 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
412 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
412 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
413 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
413 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
414 ('show_version', 'rhodecode_show_version', 'bool'),
414 ('show_version', 'rhodecode_show_version', 'bool'),
415 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
415 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
416 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
416 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
417 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
417 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
418 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
418 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
419 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
419 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
420 ('support_url', 'rhodecode_support_url', 'unicode'),
420 ('support_url', 'rhodecode_support_url', 'unicode'),
421 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
421 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
422 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
422 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
423 ]
423 ]
424 for setting, form_key, type_ in settings:
424 for setting, form_key, type_ in settings:
425 sett = SettingsModel().create_or_update_setting(
425 sett = SettingsModel().create_or_update_setting(
426 setting, form_result[form_key], type_)
426 setting, form_result[form_key], type_)
427 Session().add(sett)
427 Session().add(sett)
428
428
429 Session().commit()
429 Session().commit()
430 SettingsModel().invalidate_settings_cache()
430 SettingsModel().invalidate_settings_cache()
431 h.flash(_('Updated visualisation settings'), category='success')
431 h.flash(_('Updated visualisation settings'), category='success')
432 except Exception:
432 except Exception:
433 log.exception("Exception updating visualization settings")
433 log.exception("Exception updating visualization settings")
434 h.flash(_('Error occurred during updating '
434 h.flash(_('Error occurred during updating '
435 'visualisation settings'),
435 'visualisation settings'),
436 category='error')
436 category='error')
437
437
438 raise HTTPFound(h.route_path('admin_settings_visual'))
438 raise HTTPFound(h.route_path('admin_settings_visual'))
439
439
440 @LoginRequired()
440 @LoginRequired()
441 @HasPermissionAllDecorator('hg.admin')
441 @HasPermissionAllDecorator('hg.admin')
442 @view_config(
442 @view_config(
443 route_name='admin_settings_issuetracker', request_method='GET',
443 route_name='admin_settings_issuetracker', request_method='GET',
444 renderer='rhodecode:templates/admin/settings/settings.mako')
444 renderer='rhodecode:templates/admin/settings/settings.mako')
445 def settings_issuetracker(self):
445 def settings_issuetracker(self):
446 c = self.load_default_context()
446 c = self.load_default_context()
447 c.active = 'issuetracker'
447 c.active = 'issuetracker'
448 defaults = c.rc_config
448 defaults = c.rc_config
449
449
450 entry_key = 'rhodecode_issuetracker_pat_'
450 entry_key = 'rhodecode_issuetracker_pat_'
451
451
452 c.issuetracker_entries = {}
452 c.issuetracker_entries = {}
453 for k, v in defaults.items():
453 for k, v in defaults.items():
454 if k.startswith(entry_key):
454 if k.startswith(entry_key):
455 uid = k[len(entry_key):]
455 uid = k[len(entry_key):]
456 c.issuetracker_entries[uid] = None
456 c.issuetracker_entries[uid] = None
457
457
458 for uid in c.issuetracker_entries:
458 for uid in c.issuetracker_entries:
459 c.issuetracker_entries[uid] = AttributeDict({
459 c.issuetracker_entries[uid] = AttributeDict({
460 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
460 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
461 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
461 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
462 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
462 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
463 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
463 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
464 })
464 })
465
465
466 return self._get_template_context(c)
466 return self._get_template_context(c)
467
467
468 @LoginRequired()
468 @LoginRequired()
469 @HasPermissionAllDecorator('hg.admin')
469 @HasPermissionAllDecorator('hg.admin')
470 @CSRFRequired()
470 @CSRFRequired()
471 @view_config(
471 @view_config(
472 route_name='admin_settings_issuetracker_test', request_method='POST',
472 route_name='admin_settings_issuetracker_test', request_method='POST',
473 renderer='string', xhr=True)
473 renderer='string', xhr=True)
474 def settings_issuetracker_test(self):
474 def settings_issuetracker_test(self):
475 return h.urlify_commit_message(
475 return h.urlify_commit_message(
476 self.request.POST.get('test_text', ''),
476 self.request.POST.get('test_text', ''),
477 'repo_group/test_repo1')
477 'repo_group/test_repo1')
478
478
479 @LoginRequired()
479 @LoginRequired()
480 @HasPermissionAllDecorator('hg.admin')
480 @HasPermissionAllDecorator('hg.admin')
481 @CSRFRequired()
481 @CSRFRequired()
482 @view_config(
482 @view_config(
483 route_name='admin_settings_issuetracker_update', request_method='POST',
483 route_name='admin_settings_issuetracker_update', request_method='POST',
484 renderer='rhodecode:templates/admin/settings/settings.mako')
484 renderer='rhodecode:templates/admin/settings/settings.mako')
485 def settings_issuetracker_update(self):
485 def settings_issuetracker_update(self):
486 _ = self.request.translate
486 _ = self.request.translate
487 self.load_default_context()
487 self.load_default_context()
488 settings_model = IssueTrackerSettingsModel()
488 settings_model = IssueTrackerSettingsModel()
489
489
490 try:
490 try:
491 form = IssueTrackerPatternsForm(self.request.translate)()
491 form = IssueTrackerPatternsForm(self.request.translate)()
492 data = form.to_python(self.request.POST)
492 data = form.to_python(self.request.POST)
493 except formencode.Invalid as errors:
493 except formencode.Invalid as errors:
494 log.exception('Failed to add new pattern')
494 log.exception('Failed to add new pattern')
495 error = errors
495 error = errors
496 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
496 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
497 category='error')
497 category='error')
498 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
498 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
499
499
500 if data:
500 if data:
501 for uid in data.get('delete_patterns', []):
501 for uid in data.get('delete_patterns', []):
502 settings_model.delete_entries(uid)
502 settings_model.delete_entries(uid)
503
503
504 for pattern in data.get('patterns', []):
504 for pattern in data.get('patterns', []):
505 for setting, value, type_ in pattern:
505 for setting, value, type_ in pattern:
506 sett = settings_model.create_or_update_setting(
506 sett = settings_model.create_or_update_setting(
507 setting, value, type_)
507 setting, value, type_)
508 Session().add(sett)
508 Session().add(sett)
509
509
510 Session().commit()
510 Session().commit()
511
511
512 SettingsModel().invalidate_settings_cache()
512 SettingsModel().invalidate_settings_cache()
513 h.flash(_('Updated issue tracker entries'), category='success')
513 h.flash(_('Updated issue tracker entries'), category='success')
514 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
514 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
515
515
516 @LoginRequired()
516 @LoginRequired()
517 @HasPermissionAllDecorator('hg.admin')
517 @HasPermissionAllDecorator('hg.admin')
518 @CSRFRequired()
518 @CSRFRequired()
519 @view_config(
519 @view_config(
520 route_name='admin_settings_issuetracker_delete', request_method='POST',
520 route_name='admin_settings_issuetracker_delete', request_method='POST',
521 renderer='json_ext', xhr=True)
521 renderer='json_ext', xhr=True)
522 def settings_issuetracker_delete(self):
522 def settings_issuetracker_delete(self):
523 _ = self.request.translate
523 _ = self.request.translate
524 self.load_default_context()
524 self.load_default_context()
525 uid = self.request.POST.get('uid')
525 uid = self.request.POST.get('uid')
526 try:
526 try:
527 IssueTrackerSettingsModel().delete_entries(uid)
527 IssueTrackerSettingsModel().delete_entries(uid)
528 except Exception:
528 except Exception:
529 log.exception('Failed to delete issue tracker setting %s', uid)
529 log.exception('Failed to delete issue tracker setting %s', uid)
530 raise HTTPNotFound()
530 raise HTTPNotFound()
531
531
532 SettingsModel().invalidate_settings_cache()
532 SettingsModel().invalidate_settings_cache()
533 h.flash(_('Removed issue tracker entry.'), category='success')
533 h.flash(_('Removed issue tracker entry.'), category='success')
534
534
535 return {'deleted': uid}
535 return {'deleted': uid}
536
536
537 @LoginRequired()
537 @LoginRequired()
538 @HasPermissionAllDecorator('hg.admin')
538 @HasPermissionAllDecorator('hg.admin')
539 @view_config(
539 @view_config(
540 route_name='admin_settings_email', request_method='GET',
540 route_name='admin_settings_email', request_method='GET',
541 renderer='rhodecode:templates/admin/settings/settings.mako')
541 renderer='rhodecode:templates/admin/settings/settings.mako')
542 def settings_email(self):
542 def settings_email(self):
543 c = self.load_default_context()
543 c = self.load_default_context()
544 c.active = 'email'
544 c.active = 'email'
545 c.rhodecode_ini = rhodecode.CONFIG
545 c.rhodecode_ini = rhodecode.CONFIG
546
546
547 data = render('rhodecode:templates/admin/settings/settings.mako',
547 data = render('rhodecode:templates/admin/settings/settings.mako',
548 self._get_template_context(c), self.request)
548 self._get_template_context(c), self.request)
549 html = formencode.htmlfill.render(
549 html = formencode.htmlfill.render(
550 data,
550 data,
551 defaults=self._form_defaults(),
551 defaults=self._form_defaults(),
552 encoding="UTF-8",
552 encoding="UTF-8",
553 force_defaults=False
553 force_defaults=False
554 )
554 )
555 return Response(html)
555 return Response(html)
556
556
557 @LoginRequired()
557 @LoginRequired()
558 @HasPermissionAllDecorator('hg.admin')
558 @HasPermissionAllDecorator('hg.admin')
559 @CSRFRequired()
559 @CSRFRequired()
560 @view_config(
560 @view_config(
561 route_name='admin_settings_email_update', request_method='POST',
561 route_name='admin_settings_email_update', request_method='POST',
562 renderer='rhodecode:templates/admin/settings/settings.mako')
562 renderer='rhodecode:templates/admin/settings/settings.mako')
563 def settings_email_update(self):
563 def settings_email_update(self):
564 _ = self.request.translate
564 _ = self.request.translate
565 c = self.load_default_context()
565 c = self.load_default_context()
566 c.active = 'email'
566 c.active = 'email'
567
567
568 test_email = self.request.POST.get('test_email')
568 test_email = self.request.POST.get('test_email')
569
569
570 if not test_email:
570 if not test_email:
571 h.flash(_('Please enter email address'), category='error')
571 h.flash(_('Please enter email address'), category='error')
572 raise HTTPFound(h.route_path('admin_settings_email'))
572 raise HTTPFound(h.route_path('admin_settings_email'))
573
573
574 email_kwargs = {
574 email_kwargs = {
575 'date': datetime.datetime.now(),
575 'date': datetime.datetime.now(),
576 'user': c.rhodecode_user
576 'user': self._rhodecode_db_user
577 }
577 }
578
578
579 (subject, headers, email_body,
579 (subject, headers, email_body,
580 email_body_plaintext) = EmailNotificationModel().render_email(
580 email_body_plaintext) = EmailNotificationModel().render_email(
581 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
581 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
582
582
583 recipients = [test_email] if test_email else None
583 recipients = [test_email] if test_email else None
584
584
585 run_task(tasks.send_email, recipients, subject,
585 run_task(tasks.send_email, recipients, subject,
586 email_body_plaintext, email_body)
586 email_body_plaintext, email_body)
587
587
588 h.flash(_('Send email task created'), category='success')
588 h.flash(_('Send email task created'), category='success')
589 raise HTTPFound(h.route_path('admin_settings_email'))
589 raise HTTPFound(h.route_path('admin_settings_email'))
590
590
591 @LoginRequired()
591 @LoginRequired()
592 @HasPermissionAllDecorator('hg.admin')
592 @HasPermissionAllDecorator('hg.admin')
593 @view_config(
593 @view_config(
594 route_name='admin_settings_hooks', request_method='GET',
594 route_name='admin_settings_hooks', request_method='GET',
595 renderer='rhodecode:templates/admin/settings/settings.mako')
595 renderer='rhodecode:templates/admin/settings/settings.mako')
596 def settings_hooks(self):
596 def settings_hooks(self):
597 c = self.load_default_context()
597 c = self.load_default_context()
598 c.active = 'hooks'
598 c.active = 'hooks'
599
599
600 model = SettingsModel()
600 model = SettingsModel()
601 c.hooks = model.get_builtin_hooks()
601 c.hooks = model.get_builtin_hooks()
602 c.custom_hooks = model.get_custom_hooks()
602 c.custom_hooks = model.get_custom_hooks()
603
603
604 data = render('rhodecode:templates/admin/settings/settings.mako',
604 data = render('rhodecode:templates/admin/settings/settings.mako',
605 self._get_template_context(c), self.request)
605 self._get_template_context(c), self.request)
606 html = formencode.htmlfill.render(
606 html = formencode.htmlfill.render(
607 data,
607 data,
608 defaults=self._form_defaults(),
608 defaults=self._form_defaults(),
609 encoding="UTF-8",
609 encoding="UTF-8",
610 force_defaults=False
610 force_defaults=False
611 )
611 )
612 return Response(html)
612 return Response(html)
613
613
614 @LoginRequired()
614 @LoginRequired()
615 @HasPermissionAllDecorator('hg.admin')
615 @HasPermissionAllDecorator('hg.admin')
616 @CSRFRequired()
616 @CSRFRequired()
617 @view_config(
617 @view_config(
618 route_name='admin_settings_hooks_update', request_method='POST',
618 route_name='admin_settings_hooks_update', request_method='POST',
619 renderer='rhodecode:templates/admin/settings/settings.mako')
619 renderer='rhodecode:templates/admin/settings/settings.mako')
620 @view_config(
620 @view_config(
621 route_name='admin_settings_hooks_delete', request_method='POST',
621 route_name='admin_settings_hooks_delete', request_method='POST',
622 renderer='rhodecode:templates/admin/settings/settings.mako')
622 renderer='rhodecode:templates/admin/settings/settings.mako')
623 def settings_hooks_update(self):
623 def settings_hooks_update(self):
624 _ = self.request.translate
624 _ = self.request.translate
625 c = self.load_default_context()
625 c = self.load_default_context()
626 c.active = 'hooks'
626 c.active = 'hooks'
627 if c.visual.allow_custom_hooks_settings:
627 if c.visual.allow_custom_hooks_settings:
628 ui_key = self.request.POST.get('new_hook_ui_key')
628 ui_key = self.request.POST.get('new_hook_ui_key')
629 ui_value = self.request.POST.get('new_hook_ui_value')
629 ui_value = self.request.POST.get('new_hook_ui_value')
630
630
631 hook_id = self.request.POST.get('hook_id')
631 hook_id = self.request.POST.get('hook_id')
632 new_hook = False
632 new_hook = False
633
633
634 model = SettingsModel()
634 model = SettingsModel()
635 try:
635 try:
636 if ui_value and ui_key:
636 if ui_value and ui_key:
637 model.create_or_update_hook(ui_key, ui_value)
637 model.create_or_update_hook(ui_key, ui_value)
638 h.flash(_('Added new hook'), category='success')
638 h.flash(_('Added new hook'), category='success')
639 new_hook = True
639 new_hook = True
640 elif hook_id:
640 elif hook_id:
641 RhodeCodeUi.delete(hook_id)
641 RhodeCodeUi.delete(hook_id)
642 Session().commit()
642 Session().commit()
643
643
644 # check for edits
644 # check for edits
645 update = False
645 update = False
646 _d = self.request.POST.dict_of_lists()
646 _d = self.request.POST.dict_of_lists()
647 for k, v in zip(_d.get('hook_ui_key', []),
647 for k, v in zip(_d.get('hook_ui_key', []),
648 _d.get('hook_ui_value_new', [])):
648 _d.get('hook_ui_value_new', [])):
649 model.create_or_update_hook(k, v)
649 model.create_or_update_hook(k, v)
650 update = True
650 update = True
651
651
652 if update and not new_hook:
652 if update and not new_hook:
653 h.flash(_('Updated hooks'), category='success')
653 h.flash(_('Updated hooks'), category='success')
654 Session().commit()
654 Session().commit()
655 except Exception:
655 except Exception:
656 log.exception("Exception during hook creation")
656 log.exception("Exception during hook creation")
657 h.flash(_('Error occurred during hook creation'),
657 h.flash(_('Error occurred during hook creation'),
658 category='error')
658 category='error')
659
659
660 raise HTTPFound(h.route_path('admin_settings_hooks'))
660 raise HTTPFound(h.route_path('admin_settings_hooks'))
661
661
662 @LoginRequired()
662 @LoginRequired()
663 @HasPermissionAllDecorator('hg.admin')
663 @HasPermissionAllDecorator('hg.admin')
664 @view_config(
664 @view_config(
665 route_name='admin_settings_search', request_method='GET',
665 route_name='admin_settings_search', request_method='GET',
666 renderer='rhodecode:templates/admin/settings/settings.mako')
666 renderer='rhodecode:templates/admin/settings/settings.mako')
667 def settings_search(self):
667 def settings_search(self):
668 c = self.load_default_context()
668 c = self.load_default_context()
669 c.active = 'search'
669 c.active = 'search'
670
670
671 c.searcher = searcher_from_config(self.request.registry.settings)
671 c.searcher = searcher_from_config(self.request.registry.settings)
672 c.statistics = c.searcher.statistics(self.request.translate)
672 c.statistics = c.searcher.statistics(self.request.translate)
673
673
674 return self._get_template_context(c)
674 return self._get_template_context(c)
675
675
676 @LoginRequired()
676 @LoginRequired()
677 @HasPermissionAllDecorator('hg.admin')
677 @HasPermissionAllDecorator('hg.admin')
678 @view_config(
678 @view_config(
679 route_name='admin_settings_automation', request_method='GET',
679 route_name='admin_settings_automation', request_method='GET',
680 renderer='rhodecode:templates/admin/settings/settings.mako')
680 renderer='rhodecode:templates/admin/settings/settings.mako')
681 def settings_automation(self):
681 def settings_automation(self):
682 c = self.load_default_context()
682 c = self.load_default_context()
683 c.active = 'automation'
683 c.active = 'automation'
684
684
685 return self._get_template_context(c)
685 return self._get_template_context(c)
686
686
687 @LoginRequired()
687 @LoginRequired()
688 @HasPermissionAllDecorator('hg.admin')
688 @HasPermissionAllDecorator('hg.admin')
689 @view_config(
689 @view_config(
690 route_name='admin_settings_labs', request_method='GET',
690 route_name='admin_settings_labs', request_method='GET',
691 renderer='rhodecode:templates/admin/settings/settings.mako')
691 renderer='rhodecode:templates/admin/settings/settings.mako')
692 def settings_labs(self):
692 def settings_labs(self):
693 c = self.load_default_context()
693 c = self.load_default_context()
694 if not c.labs_active:
694 if not c.labs_active:
695 raise HTTPFound(h.route_path('admin_settings'))
695 raise HTTPFound(h.route_path('admin_settings'))
696
696
697 c.active = 'labs'
697 c.active = 'labs'
698 c.lab_settings = _LAB_SETTINGS
698 c.lab_settings = _LAB_SETTINGS
699
699
700 data = render('rhodecode:templates/admin/settings/settings.mako',
700 data = render('rhodecode:templates/admin/settings/settings.mako',
701 self._get_template_context(c), self.request)
701 self._get_template_context(c), self.request)
702 html = formencode.htmlfill.render(
702 html = formencode.htmlfill.render(
703 data,
703 data,
704 defaults=self._form_defaults(),
704 defaults=self._form_defaults(),
705 encoding="UTF-8",
705 encoding="UTF-8",
706 force_defaults=False
706 force_defaults=False
707 )
707 )
708 return Response(html)
708 return Response(html)
709
709
710 @LoginRequired()
710 @LoginRequired()
711 @HasPermissionAllDecorator('hg.admin')
711 @HasPermissionAllDecorator('hg.admin')
712 @CSRFRequired()
712 @CSRFRequired()
713 @view_config(
713 @view_config(
714 route_name='admin_settings_labs_update', request_method='POST',
714 route_name='admin_settings_labs_update', request_method='POST',
715 renderer='rhodecode:templates/admin/settings/settings.mako')
715 renderer='rhodecode:templates/admin/settings/settings.mako')
716 def settings_labs_update(self):
716 def settings_labs_update(self):
717 _ = self.request.translate
717 _ = self.request.translate
718 c = self.load_default_context()
718 c = self.load_default_context()
719 c.active = 'labs'
719 c.active = 'labs'
720
720
721 application_form = LabsSettingsForm(self.request.translate)()
721 application_form = LabsSettingsForm(self.request.translate)()
722 try:
722 try:
723 form_result = application_form.to_python(dict(self.request.POST))
723 form_result = application_form.to_python(dict(self.request.POST))
724 except formencode.Invalid as errors:
724 except formencode.Invalid as errors:
725 h.flash(
725 h.flash(
726 _("Some form inputs contain invalid data."),
726 _("Some form inputs contain invalid data."),
727 category='error')
727 category='error')
728 data = render('rhodecode:templates/admin/settings/settings.mako',
728 data = render('rhodecode:templates/admin/settings/settings.mako',
729 self._get_template_context(c), self.request)
729 self._get_template_context(c), self.request)
730 html = formencode.htmlfill.render(
730 html = formencode.htmlfill.render(
731 data,
731 data,
732 defaults=errors.value,
732 defaults=errors.value,
733 errors=errors.error_dict or {},
733 errors=errors.error_dict or {},
734 prefix_error=False,
734 prefix_error=False,
735 encoding="UTF-8",
735 encoding="UTF-8",
736 force_defaults=False
736 force_defaults=False
737 )
737 )
738 return Response(html)
738 return Response(html)
739
739
740 try:
740 try:
741 session = Session()
741 session = Session()
742 for setting in _LAB_SETTINGS:
742 for setting in _LAB_SETTINGS:
743 setting_name = setting.key[len('rhodecode_'):]
743 setting_name = setting.key[len('rhodecode_'):]
744 sett = SettingsModel().create_or_update_setting(
744 sett = SettingsModel().create_or_update_setting(
745 setting_name, form_result[setting.key], setting.type)
745 setting_name, form_result[setting.key], setting.type)
746 session.add(sett)
746 session.add(sett)
747
747
748 except Exception:
748 except Exception:
749 log.exception('Exception while updating lab settings')
749 log.exception('Exception while updating lab settings')
750 h.flash(_('Error occurred during updating labs settings'),
750 h.flash(_('Error occurred during updating labs settings'),
751 category='error')
751 category='error')
752 else:
752 else:
753 Session().commit()
753 Session().commit()
754 SettingsModel().invalidate_settings_cache()
754 SettingsModel().invalidate_settings_cache()
755 h.flash(_('Updated Labs settings'), category='success')
755 h.flash(_('Updated Labs settings'), category='success')
756 raise HTTPFound(h.route_path('admin_settings_labs'))
756 raise HTTPFound(h.route_path('admin_settings_labs'))
757
757
758 data = render('rhodecode:templates/admin/settings/settings.mako',
758 data = render('rhodecode:templates/admin/settings/settings.mako',
759 self._get_template_context(c), self.request)
759 self._get_template_context(c), self.request)
760 html = formencode.htmlfill.render(
760 html = formencode.htmlfill.render(
761 data,
761 data,
762 defaults=self._form_defaults(),
762 defaults=self._form_defaults(),
763 encoding="UTF-8",
763 encoding="UTF-8",
764 force_defaults=False
764 force_defaults=False
765 )
765 )
766 return Response(html)
766 return Response(html)
767
767
768
768
769 # :param key: name of the setting including the 'rhodecode_' prefix
769 # :param key: name of the setting including the 'rhodecode_' prefix
770 # :param type: the RhodeCodeSetting type to use.
770 # :param type: the RhodeCodeSetting type to use.
771 # :param group: the i18ned group in which we should dispaly this setting
771 # :param group: the i18ned group in which we should dispaly this setting
772 # :param label: the i18ned label we should display for this setting
772 # :param label: the i18ned label we should display for this setting
773 # :param help: the i18ned help we should dispaly for this setting
773 # :param help: the i18ned help we should dispaly for this setting
774 LabSetting = collections.namedtuple(
774 LabSetting = collections.namedtuple(
775 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
775 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
776
776
777
777
778 # This list has to be kept in sync with the form
778 # This list has to be kept in sync with the form
779 # rhodecode.model.forms.LabsSettingsForm.
779 # rhodecode.model.forms.LabsSettingsForm.
780 _LAB_SETTINGS = [
780 _LAB_SETTINGS = [
781
781
782 ]
782 ]
@@ -1,1333 +1,1336 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.plugins import auth_rhodecode
35 from rhodecode.authentication.plugins import auth_rhodecode
36 from rhodecode.events import trigger
36 from rhodecode.events import trigger
37 from rhodecode.model.db import true
37 from rhodecode.model.db import true
38
38
39 from rhodecode.lib import audit_logger, rc_cache
39 from rhodecode.lib import audit_logger, rc_cache
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 UserOwnsUserGroupsException, DefaultUserException)
42 UserOwnsUserGroupsException, DefaultUserException)
43 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 from rhodecode.lib import helpers as h
46 from rhodecode.lib import helpers as h
47 from rhodecode.lib.helpers import SqlPage
47 from rhodecode.lib.helpers import SqlPage
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.forms import (
50 from rhodecode.model.forms import (
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 UserExtraEmailForm, UserExtraIpForm)
52 UserExtraEmailForm, UserExtraIpForm)
53 from rhodecode.model.permission import PermissionModel
53 from rhodecode.model.permission import PermissionModel
54 from rhodecode.model.repo_group import RepoGroupModel
54 from rhodecode.model.repo_group import RepoGroupModel
55 from rhodecode.model.ssh_key import SshKeyModel
55 from rhodecode.model.ssh_key import SshKeyModel
56 from rhodecode.model.user import UserModel
56 from rhodecode.model.user import UserModel
57 from rhodecode.model.user_group import UserGroupModel
57 from rhodecode.model.user_group import UserGroupModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 UserApiKeys, UserSshKeys, RepoGroup)
60 UserApiKeys, UserSshKeys, RepoGroup)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 class AdminUsersView(BaseAppView, DataGridAppView):
66 class AdminUsersView(BaseAppView, DataGridAppView):
67
67
68 def load_default_context(self):
68 def load_default_context(self):
69 c = self._get_local_tmpl_context()
69 c = self._get_local_tmpl_context()
70 return c
70 return c
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @HasPermissionAllDecorator('hg.admin')
73 @HasPermissionAllDecorator('hg.admin')
74 @view_config(
74 @view_config(
75 route_name='users', request_method='GET',
75 route_name='users', request_method='GET',
76 renderer='rhodecode:templates/admin/users/users.mako')
76 renderer='rhodecode:templates/admin/users/users.mako')
77 def users_list(self):
77 def users_list(self):
78 c = self.load_default_context()
78 c = self.load_default_context()
79 return self._get_template_context(c)
79 return self._get_template_context(c)
80
80
81 @LoginRequired()
81 @LoginRequired()
82 @HasPermissionAllDecorator('hg.admin')
82 @HasPermissionAllDecorator('hg.admin')
83 @view_config(
83 @view_config(
84 # renderer defined below
84 # renderer defined below
85 route_name='users_data', request_method='GET',
85 route_name='users_data', request_method='GET',
86 renderer='json_ext', xhr=True)
86 renderer='json_ext', xhr=True)
87 def users_list_data(self):
87 def users_list_data(self):
88 self.load_default_context()
88 self.load_default_context()
89 column_map = {
89 column_map = {
90 'first_name': 'name',
90 'first_name': 'name',
91 'last_name': 'lastname',
91 'last_name': 'lastname',
92 }
92 }
93 draw, start, limit = self._extract_chunk(self.request)
93 draw, start, limit = self._extract_chunk(self.request)
94 search_q, order_by, order_dir = self._extract_ordering(
94 search_q, order_by, order_dir = self._extract_ordering(
95 self.request, column_map=column_map)
95 self.request, column_map=column_map)
96 _render = self.request.get_partial_renderer(
96 _render = self.request.get_partial_renderer(
97 'rhodecode:templates/data_table/_dt_elements.mako')
97 'rhodecode:templates/data_table/_dt_elements.mako')
98
98
99 def user_actions(user_id, username):
99 def user_actions(user_id, username):
100 return _render("user_actions", user_id, username)
100 return _render("user_actions", user_id, username)
101
101
102 users_data_total_count = User.query()\
102 users_data_total_count = User.query()\
103 .filter(User.username != User.DEFAULT_USER) \
103 .filter(User.username != User.DEFAULT_USER) \
104 .count()
104 .count()
105
105
106 users_data_total_inactive_count = User.query()\
106 users_data_total_inactive_count = User.query()\
107 .filter(User.username != User.DEFAULT_USER) \
107 .filter(User.username != User.DEFAULT_USER) \
108 .filter(User.active != true())\
108 .filter(User.active != true())\
109 .count()
109 .count()
110
110
111 # json generate
111 # json generate
112 base_q = User.query().filter(User.username != User.DEFAULT_USER)
112 base_q = User.query().filter(User.username != User.DEFAULT_USER)
113 base_inactive_q = base_q.filter(User.active != true())
113 base_inactive_q = base_q.filter(User.active != true())
114
114
115 if search_q:
115 if search_q:
116 like_expression = u'%{}%'.format(safe_unicode(search_q))
116 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 base_q = base_q.filter(or_(
117 base_q = base_q.filter(or_(
118 User.username.ilike(like_expression),
118 User.username.ilike(like_expression),
119 User._email.ilike(like_expression),
119 User._email.ilike(like_expression),
120 User.name.ilike(like_expression),
120 User.name.ilike(like_expression),
121 User.lastname.ilike(like_expression),
121 User.lastname.ilike(like_expression),
122 ))
122 ))
123 base_inactive_q = base_q.filter(User.active != true())
123 base_inactive_q = base_q.filter(User.active != true())
124
124
125 users_data_total_filtered_count = base_q.count()
125 users_data_total_filtered_count = base_q.count()
126 users_data_total_filtered_inactive_count = base_inactive_q.count()
126 users_data_total_filtered_inactive_count = base_inactive_q.count()
127
127
128 sort_col = getattr(User, order_by, None)
128 sort_col = getattr(User, order_by, None)
129 if sort_col:
129 if sort_col:
130 if order_dir == 'asc':
130 if order_dir == 'asc':
131 # handle null values properly to order by NULL last
131 # handle null values properly to order by NULL last
132 if order_by in ['last_activity']:
132 if order_by in ['last_activity']:
133 sort_col = coalesce(sort_col, datetime.date.max)
133 sort_col = coalesce(sort_col, datetime.date.max)
134 sort_col = sort_col.asc()
134 sort_col = sort_col.asc()
135 else:
135 else:
136 # handle null values properly to order by NULL last
136 # handle null values properly to order by NULL last
137 if order_by in ['last_activity']:
137 if order_by in ['last_activity']:
138 sort_col = coalesce(sort_col, datetime.date.min)
138 sort_col = coalesce(sort_col, datetime.date.min)
139 sort_col = sort_col.desc()
139 sort_col = sort_col.desc()
140
140
141 base_q = base_q.order_by(sort_col)
141 base_q = base_q.order_by(sort_col)
142 base_q = base_q.offset(start).limit(limit)
142 base_q = base_q.offset(start).limit(limit)
143
143
144 users_list = base_q.all()
144 users_list = base_q.all()
145
145
146 users_data = []
146 users_data = []
147 for user in users_list:
147 for user in users_list:
148 users_data.append({
148 users_data.append({
149 "username": h.gravatar_with_user(self.request, user.username),
149 "username": h.gravatar_with_user(self.request, user.username),
150 "email": user.email,
150 "email": user.email,
151 "first_name": user.first_name,
151 "first_name": user.first_name,
152 "last_name": user.last_name,
152 "last_name": user.last_name,
153 "last_login": h.format_date(user.last_login),
153 "last_login": h.format_date(user.last_login),
154 "last_activity": h.format_date(user.last_activity),
154 "last_activity": h.format_date(user.last_activity),
155 "active": h.bool2icon(user.active),
155 "active": h.bool2icon(user.active),
156 "active_raw": user.active,
156 "active_raw": user.active,
157 "admin": h.bool2icon(user.admin),
157 "admin": h.bool2icon(user.admin),
158 "extern_type": user.extern_type,
158 "extern_type": user.extern_type,
159 "extern_name": user.extern_name,
159 "extern_name": user.extern_name,
160 "action": user_actions(user.user_id, user.username),
160 "action": user_actions(user.user_id, user.username),
161 })
161 })
162 data = ({
162 data = ({
163 'draw': draw,
163 'draw': draw,
164 'data': users_data,
164 'data': users_data,
165 'recordsTotal': users_data_total_count,
165 'recordsTotal': users_data_total_count,
166 'recordsFiltered': users_data_total_filtered_count,
166 'recordsFiltered': users_data_total_filtered_count,
167 'recordsTotalInactive': users_data_total_inactive_count,
167 'recordsTotalInactive': users_data_total_inactive_count,
168 'recordsFilteredInactive': users_data_total_filtered_inactive_count
168 'recordsFilteredInactive': users_data_total_filtered_inactive_count
169 })
169 })
170
170
171 return data
171 return data
172
172
173 def _set_personal_repo_group_template_vars(self, c_obj):
173 def _set_personal_repo_group_template_vars(self, c_obj):
174 DummyUser = AttributeDict({
174 DummyUser = AttributeDict({
175 'username': '${username}',
175 'username': '${username}',
176 'user_id': '${user_id}',
176 'user_id': '${user_id}',
177 })
177 })
178 c_obj.default_create_repo_group = RepoGroupModel() \
178 c_obj.default_create_repo_group = RepoGroupModel() \
179 .get_default_create_personal_repo_group()
179 .get_default_create_personal_repo_group()
180 c_obj.personal_repo_group_name = RepoGroupModel() \
180 c_obj.personal_repo_group_name = RepoGroupModel() \
181 .get_personal_group_name(DummyUser)
181 .get_personal_group_name(DummyUser)
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasPermissionAllDecorator('hg.admin')
184 @HasPermissionAllDecorator('hg.admin')
185 @view_config(
185 @view_config(
186 route_name='users_new', request_method='GET',
186 route_name='users_new', request_method='GET',
187 renderer='rhodecode:templates/admin/users/user_add.mako')
187 renderer='rhodecode:templates/admin/users/user_add.mako')
188 def users_new(self):
188 def users_new(self):
189 _ = self.request.translate
189 _ = self.request.translate
190 c = self.load_default_context()
190 c = self.load_default_context()
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 self._set_personal_repo_group_template_vars(c)
192 self._set_personal_repo_group_template_vars(c)
193 return self._get_template_context(c)
193 return self._get_template_context(c)
194
194
195 @LoginRequired()
195 @LoginRequired()
196 @HasPermissionAllDecorator('hg.admin')
196 @HasPermissionAllDecorator('hg.admin')
197 @CSRFRequired()
197 @CSRFRequired()
198 @view_config(
198 @view_config(
199 route_name='users_create', request_method='POST',
199 route_name='users_create', request_method='POST',
200 renderer='rhodecode:templates/admin/users/user_add.mako')
200 renderer='rhodecode:templates/admin/users/user_add.mako')
201 def users_create(self):
201 def users_create(self):
202 _ = self.request.translate
202 _ = self.request.translate
203 c = self.load_default_context()
203 c = self.load_default_context()
204 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
204 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
205 user_model = UserModel()
205 user_model = UserModel()
206 user_form = UserForm(self.request.translate)()
206 user_form = UserForm(self.request.translate)()
207 try:
207 try:
208 form_result = user_form.to_python(dict(self.request.POST))
208 form_result = user_form.to_python(dict(self.request.POST))
209 user = user_model.create(form_result)
209 user = user_model.create(form_result)
210 Session().flush()
210 Session().flush()
211 creation_data = user.get_api_data()
211 creation_data = user.get_api_data()
212 username = form_result['username']
212 username = form_result['username']
213
213
214 audit_logger.store_web(
214 audit_logger.store_web(
215 'user.create', action_data={'data': creation_data},
215 'user.create', action_data={'data': creation_data},
216 user=c.rhodecode_user)
216 user=c.rhodecode_user)
217
217
218 user_link = h.link_to(
218 user_link = h.link_to(
219 h.escape(username),
219 h.escape(username),
220 h.route_path('user_edit', user_id=user.user_id))
220 h.route_path('user_edit', user_id=user.user_id))
221 h.flash(h.literal(_('Created user %(user_link)s')
221 h.flash(h.literal(_('Created user %(user_link)s')
222 % {'user_link': user_link}), category='success')
222 % {'user_link': user_link}), category='success')
223 Session().commit()
223 Session().commit()
224 except formencode.Invalid as errors:
224 except formencode.Invalid as errors:
225 self._set_personal_repo_group_template_vars(c)
225 self._set_personal_repo_group_template_vars(c)
226 data = render(
226 data = render(
227 'rhodecode:templates/admin/users/user_add.mako',
227 'rhodecode:templates/admin/users/user_add.mako',
228 self._get_template_context(c), self.request)
228 self._get_template_context(c), self.request)
229 html = formencode.htmlfill.render(
229 html = formencode.htmlfill.render(
230 data,
230 data,
231 defaults=errors.value,
231 defaults=errors.value,
232 errors=errors.error_dict or {},
232 errors=errors.error_dict or {},
233 prefix_error=False,
233 prefix_error=False,
234 encoding="UTF-8",
234 encoding="UTF-8",
235 force_defaults=False
235 force_defaults=False
236 )
236 )
237 return Response(html)
237 return Response(html)
238 except UserCreationError as e:
238 except UserCreationError as e:
239 h.flash(e, 'error')
239 h.flash(e, 'error')
240 except Exception:
240 except Exception:
241 log.exception("Exception creation of user")
241 log.exception("Exception creation of user")
242 h.flash(_('Error occurred during creation of user %s')
242 h.flash(_('Error occurred during creation of user %s')
243 % self.request.POST.get('username'), category='error')
243 % self.request.POST.get('username'), category='error')
244 raise HTTPFound(h.route_path('users'))
244 raise HTTPFound(h.route_path('users'))
245
245
246
246
247 class UsersView(UserAppView):
247 class UsersView(UserAppView):
248 ALLOW_SCOPED_TOKENS = False
248 ALLOW_SCOPED_TOKENS = False
249 """
249 """
250 This view has alternative version inside EE, if modified please take a look
250 This view has alternative version inside EE, if modified please take a look
251 in there as well.
251 in there as well.
252 """
252 """
253
253
254 def get_auth_plugins(self):
254 def get_auth_plugins(self):
255 valid_plugins = []
255 valid_plugins = []
256 authn_registry = get_authn_registry(self.request.registry)
256 authn_registry = get_authn_registry(self.request.registry)
257 for plugin in authn_registry.get_plugins_for_authentication():
257 for plugin in authn_registry.get_plugins_for_authentication():
258 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
258 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
259 valid_plugins.append(plugin)
259 valid_plugins.append(plugin)
260 elif plugin.name == 'rhodecode':
260 elif plugin.name == 'rhodecode':
261 valid_plugins.append(plugin)
261 valid_plugins.append(plugin)
262
262
263 # extend our choices if user has set a bound plugin which isn't enabled at the
263 # extend our choices if user has set a bound plugin which isn't enabled at the
264 # moment
264 # moment
265 extern_type = self.db_user.extern_type
265 extern_type = self.db_user.extern_type
266 if extern_type not in [x.uid for x in valid_plugins]:
266 if extern_type not in [x.uid for x in valid_plugins]:
267 try:
267 try:
268 plugin = authn_registry.get_plugin_by_uid(extern_type)
268 plugin = authn_registry.get_plugin_by_uid(extern_type)
269 if plugin:
269 if plugin:
270 valid_plugins.append(plugin)
270 valid_plugins.append(plugin)
271
271
272 except Exception:
272 except Exception:
273 log.exception(
273 log.exception(
274 'Could not extend user plugins with `{}`'.format(extern_type))
274 'Could not extend user plugins with `{}`'.format(extern_type))
275 return valid_plugins
275 return valid_plugins
276
276
277 def load_default_context(self):
277 def load_default_context(self):
278 req = self.request
278 req = self.request
279
279
280 c = self._get_local_tmpl_context()
280 c = self._get_local_tmpl_context()
281 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
281 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
282 c.allowed_languages = [
282 c.allowed_languages = [
283 ('en', 'English (en)'),
283 ('en', 'English (en)'),
284 ('de', 'German (de)'),
284 ('de', 'German (de)'),
285 ('fr', 'French (fr)'),
285 ('fr', 'French (fr)'),
286 ('it', 'Italian (it)'),
286 ('it', 'Italian (it)'),
287 ('ja', 'Japanese (ja)'),
287 ('ja', 'Japanese (ja)'),
288 ('pl', 'Polish (pl)'),
288 ('pl', 'Polish (pl)'),
289 ('pt', 'Portuguese (pt)'),
289 ('pt', 'Portuguese (pt)'),
290 ('ru', 'Russian (ru)'),
290 ('ru', 'Russian (ru)'),
291 ('zh', 'Chinese (zh)'),
291 ('zh', 'Chinese (zh)'),
292 ]
292 ]
293
293
294 c.allowed_extern_types = [
294 c.allowed_extern_types = [
295 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
295 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
296 ]
296 ]
297
297
298 c.available_permissions = req.registry.settings['available_permissions']
298 c.available_permissions = req.registry.settings['available_permissions']
299 PermissionModel().set_global_permission_choices(
299 PermissionModel().set_global_permission_choices(
300 c, gettext_translator=req.translate)
300 c, gettext_translator=req.translate)
301
301
302 return c
302 return c
303
303
304 @LoginRequired()
304 @LoginRequired()
305 @HasPermissionAllDecorator('hg.admin')
305 @HasPermissionAllDecorator('hg.admin')
306 @CSRFRequired()
306 @CSRFRequired()
307 @view_config(
307 @view_config(
308 route_name='user_update', request_method='POST',
308 route_name='user_update', request_method='POST',
309 renderer='rhodecode:templates/admin/users/user_edit.mako')
309 renderer='rhodecode:templates/admin/users/user_edit.mako')
310 def user_update(self):
310 def user_update(self):
311 _ = self.request.translate
311 _ = self.request.translate
312 c = self.load_default_context()
312 c = self.load_default_context()
313
313
314 user_id = self.db_user_id
314 user_id = self.db_user_id
315 c.user = self.db_user
315 c.user = self.db_user
316
316
317 c.active = 'profile'
317 c.active = 'profile'
318 c.extern_type = c.user.extern_type
318 c.extern_type = c.user.extern_type
319 c.extern_name = c.user.extern_name
319 c.extern_name = c.user.extern_name
320 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
320 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
321 available_languages = [x[0] for x in c.allowed_languages]
321 available_languages = [x[0] for x in c.allowed_languages]
322 _form = UserForm(self.request.translate, edit=True,
322 _form = UserForm(self.request.translate, edit=True,
323 available_languages=available_languages,
323 available_languages=available_languages,
324 old_data={'user_id': user_id,
324 old_data={'user_id': user_id,
325 'email': c.user.email})()
325 'email': c.user.email})()
326 form_result = {}
326 form_result = {}
327 old_values = c.user.get_api_data()
327 old_values = c.user.get_api_data()
328 try:
328 try:
329 form_result = _form.to_python(dict(self.request.POST))
329 form_result = _form.to_python(dict(self.request.POST))
330 skip_attrs = ['extern_name']
330 skip_attrs = ['extern_name']
331 # TODO: plugin should define if username can be updated
331 # TODO: plugin should define if username can be updated
332 if c.extern_type != "rhodecode":
332 if c.extern_type != "rhodecode":
333 # forbid updating username for external accounts
333 # forbid updating username for external accounts
334 skip_attrs.append('username')
334 skip_attrs.append('username')
335
335
336 UserModel().update_user(
336 UserModel().update_user(
337 user_id, skip_attrs=skip_attrs, **form_result)
337 user_id, skip_attrs=skip_attrs, **form_result)
338
338
339 audit_logger.store_web(
339 audit_logger.store_web(
340 'user.edit', action_data={'old_data': old_values},
340 'user.edit', action_data={'old_data': old_values},
341 user=c.rhodecode_user)
341 user=c.rhodecode_user)
342
342
343 Session().commit()
343 Session().commit()
344 h.flash(_('User updated successfully'), category='success')
344 h.flash(_('User updated successfully'), category='success')
345 except formencode.Invalid as errors:
345 except formencode.Invalid as errors:
346 data = render(
346 data = render(
347 'rhodecode:templates/admin/users/user_edit.mako',
347 'rhodecode:templates/admin/users/user_edit.mako',
348 self._get_template_context(c), self.request)
348 self._get_template_context(c), self.request)
349 html = formencode.htmlfill.render(
349 html = formencode.htmlfill.render(
350 data,
350 data,
351 defaults=errors.value,
351 defaults=errors.value,
352 errors=errors.error_dict or {},
352 errors=errors.error_dict or {},
353 prefix_error=False,
353 prefix_error=False,
354 encoding="UTF-8",
354 encoding="UTF-8",
355 force_defaults=False
355 force_defaults=False
356 )
356 )
357 return Response(html)
357 return Response(html)
358 except UserCreationError as e:
358 except UserCreationError as e:
359 h.flash(e, 'error')
359 h.flash(e, 'error')
360 except Exception:
360 except Exception:
361 log.exception("Exception updating user")
361 log.exception("Exception updating user")
362 h.flash(_('Error occurred during update of user %s')
362 h.flash(_('Error occurred during update of user %s')
363 % form_result.get('username'), category='error')
363 % form_result.get('username'), category='error')
364 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
364 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
365
365
366 @LoginRequired()
366 @LoginRequired()
367 @HasPermissionAllDecorator('hg.admin')
367 @HasPermissionAllDecorator('hg.admin')
368 @CSRFRequired()
368 @CSRFRequired()
369 @view_config(
369 @view_config(
370 route_name='user_delete', request_method='POST',
370 route_name='user_delete', request_method='POST',
371 renderer='rhodecode:templates/admin/users/user_edit.mako')
371 renderer='rhodecode:templates/admin/users/user_edit.mako')
372 def user_delete(self):
372 def user_delete(self):
373 _ = self.request.translate
373 _ = self.request.translate
374 c = self.load_default_context()
374 c = self.load_default_context()
375 c.user = self.db_user
375 c.user = self.db_user
376
376
377 _repos = c.user.repositories
377 _repos = c.user.repositories
378 _repo_groups = c.user.repository_groups
378 _repo_groups = c.user.repository_groups
379 _user_groups = c.user.user_groups
379 _user_groups = c.user.user_groups
380 _artifacts = c.user.artifacts
380 _artifacts = c.user.artifacts
381
381
382 handle_repos = None
382 handle_repos = None
383 handle_repo_groups = None
383 handle_repo_groups = None
384 handle_user_groups = None
384 handle_user_groups = None
385 handle_artifacts = None
385 handle_artifacts = None
386
386
387 # calls for flash of handle based on handle case detach or delete
387 # calls for flash of handle based on handle case detach or delete
388 def set_handle_flash_repos():
388 def set_handle_flash_repos():
389 handle = handle_repos
389 handle = handle_repos
390 if handle == 'detach':
390 if handle == 'detach':
391 h.flash(_('Detached %s repositories') % len(_repos),
391 h.flash(_('Detached %s repositories') % len(_repos),
392 category='success')
392 category='success')
393 elif handle == 'delete':
393 elif handle == 'delete':
394 h.flash(_('Deleted %s repositories') % len(_repos),
394 h.flash(_('Deleted %s repositories') % len(_repos),
395 category='success')
395 category='success')
396
396
397 def set_handle_flash_repo_groups():
397 def set_handle_flash_repo_groups():
398 handle = handle_repo_groups
398 handle = handle_repo_groups
399 if handle == 'detach':
399 if handle == 'detach':
400 h.flash(_('Detached %s repository groups') % len(_repo_groups),
400 h.flash(_('Detached %s repository groups') % len(_repo_groups),
401 category='success')
401 category='success')
402 elif handle == 'delete':
402 elif handle == 'delete':
403 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
403 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
404 category='success')
404 category='success')
405
405
406 def set_handle_flash_user_groups():
406 def set_handle_flash_user_groups():
407 handle = handle_user_groups
407 handle = handle_user_groups
408 if handle == 'detach':
408 if handle == 'detach':
409 h.flash(_('Detached %s user groups') % len(_user_groups),
409 h.flash(_('Detached %s user groups') % len(_user_groups),
410 category='success')
410 category='success')
411 elif handle == 'delete':
411 elif handle == 'delete':
412 h.flash(_('Deleted %s user groups') % len(_user_groups),
412 h.flash(_('Deleted %s user groups') % len(_user_groups),
413 category='success')
413 category='success')
414
414
415 def set_handle_flash_artifacts():
415 def set_handle_flash_artifacts():
416 handle = handle_artifacts
416 handle = handle_artifacts
417 if handle == 'detach':
417 if handle == 'detach':
418 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 h.flash(_('Detached %s artifacts') % len(_artifacts),
419 category='success')
419 category='success')
420 elif handle == 'delete':
420 elif handle == 'delete':
421 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 h.flash(_('Deleted %s artifacts') % len(_artifacts),
422 category='success')
422 category='success')
423
423
424 if _repos and self.request.POST.get('user_repos'):
424 if _repos and self.request.POST.get('user_repos'):
425 handle_repos = self.request.POST['user_repos']
425 handle_repos = self.request.POST['user_repos']
426
426
427 if _repo_groups and self.request.POST.get('user_repo_groups'):
427 if _repo_groups and self.request.POST.get('user_repo_groups'):
428 handle_repo_groups = self.request.POST['user_repo_groups']
428 handle_repo_groups = self.request.POST['user_repo_groups']
429
429
430 if _user_groups and self.request.POST.get('user_user_groups'):
430 if _user_groups and self.request.POST.get('user_user_groups'):
431 handle_user_groups = self.request.POST['user_user_groups']
431 handle_user_groups = self.request.POST['user_user_groups']
432
432
433 if _artifacts and self.request.POST.get('user_artifacts'):
433 if _artifacts and self.request.POST.get('user_artifacts'):
434 handle_artifacts = self.request.POST['user_artifacts']
434 handle_artifacts = self.request.POST['user_artifacts']
435
435
436 old_values = c.user.get_api_data()
436 old_values = c.user.get_api_data()
437
437
438 try:
438 try:
439 UserModel().delete(c.user, handle_repos=handle_repos,
439 UserModel().delete(c.user, handle_repos=handle_repos,
440 handle_repo_groups=handle_repo_groups,
440 handle_repo_groups=handle_repo_groups,
441 handle_user_groups=handle_user_groups,
441 handle_user_groups=handle_user_groups,
442 handle_artifacts=handle_artifacts)
442 handle_artifacts=handle_artifacts)
443
443
444 audit_logger.store_web(
444 audit_logger.store_web(
445 'user.delete', action_data={'old_data': old_values},
445 'user.delete', action_data={'old_data': old_values},
446 user=c.rhodecode_user)
446 user=c.rhodecode_user)
447
447
448 Session().commit()
448 Session().commit()
449 set_handle_flash_repos()
449 set_handle_flash_repos()
450 set_handle_flash_repo_groups()
450 set_handle_flash_repo_groups()
451 set_handle_flash_user_groups()
451 set_handle_flash_user_groups()
452 set_handle_flash_artifacts()
452 set_handle_flash_artifacts()
453 username = h.escape(old_values['username'])
453 username = h.escape(old_values['username'])
454 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
454 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
455 except (UserOwnsReposException, UserOwnsRepoGroupsException,
455 except (UserOwnsReposException, UserOwnsRepoGroupsException,
456 UserOwnsUserGroupsException, DefaultUserException) as e:
456 UserOwnsUserGroupsException, DefaultUserException) as e:
457 h.flash(e, category='warning')
457 h.flash(e, category='warning')
458 except Exception:
458 except Exception:
459 log.exception("Exception during deletion of user")
459 log.exception("Exception during deletion of user")
460 h.flash(_('An error occurred during deletion of user'),
460 h.flash(_('An error occurred during deletion of user'),
461 category='error')
461 category='error')
462 raise HTTPFound(h.route_path('users'))
462 raise HTTPFound(h.route_path('users'))
463
463
464 @LoginRequired()
464 @LoginRequired()
465 @HasPermissionAllDecorator('hg.admin')
465 @HasPermissionAllDecorator('hg.admin')
466 @view_config(
466 @view_config(
467 route_name='user_edit', request_method='GET',
467 route_name='user_edit', request_method='GET',
468 renderer='rhodecode:templates/admin/users/user_edit.mako')
468 renderer='rhodecode:templates/admin/users/user_edit.mako')
469 def user_edit(self):
469 def user_edit(self):
470 _ = self.request.translate
470 _ = self.request.translate
471 c = self.load_default_context()
471 c = self.load_default_context()
472 c.user = self.db_user
472 c.user = self.db_user
473
473
474 c.active = 'profile'
474 c.active = 'profile'
475 c.extern_type = c.user.extern_type
475 c.extern_type = c.user.extern_type
476 c.extern_name = c.user.extern_name
476 c.extern_name = c.user.extern_name
477 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
477 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
478
478
479 defaults = c.user.get_dict()
479 defaults = c.user.get_dict()
480 defaults.update({'language': c.user.user_data.get('language')})
480 defaults.update({'language': c.user.user_data.get('language')})
481
481
482 data = render(
482 data = render(
483 'rhodecode:templates/admin/users/user_edit.mako',
483 'rhodecode:templates/admin/users/user_edit.mako',
484 self._get_template_context(c), self.request)
484 self._get_template_context(c), self.request)
485 html = formencode.htmlfill.render(
485 html = formencode.htmlfill.render(
486 data,
486 data,
487 defaults=defaults,
487 defaults=defaults,
488 encoding="UTF-8",
488 encoding="UTF-8",
489 force_defaults=False
489 force_defaults=False
490 )
490 )
491 return Response(html)
491 return Response(html)
492
492
493 @LoginRequired()
493 @LoginRequired()
494 @HasPermissionAllDecorator('hg.admin')
494 @HasPermissionAllDecorator('hg.admin')
495 @view_config(
495 @view_config(
496 route_name='user_edit_advanced', request_method='GET',
496 route_name='user_edit_advanced', request_method='GET',
497 renderer='rhodecode:templates/admin/users/user_edit.mako')
497 renderer='rhodecode:templates/admin/users/user_edit.mako')
498 def user_edit_advanced(self):
498 def user_edit_advanced(self):
499 _ = self.request.translate
499 _ = self.request.translate
500 c = self.load_default_context()
500 c = self.load_default_context()
501
501
502 user_id = self.db_user_id
502 user_id = self.db_user_id
503 c.user = self.db_user
503 c.user = self.db_user
504
504
505 c.active = 'advanced'
505 c.active = 'advanced'
506 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
506 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
507 c.personal_repo_group_name = RepoGroupModel()\
507 c.personal_repo_group_name = RepoGroupModel()\
508 .get_personal_group_name(c.user)
508 .get_personal_group_name(c.user)
509
509
510 c.user_to_review_rules = sorted(
510 c.user_to_review_rules = sorted(
511 (x.user for x in c.user.user_review_rules),
511 (x.user for x in c.user.user_review_rules),
512 key=lambda u: u.username.lower())
512 key=lambda u: u.username.lower())
513
513
514 c.first_admin = User.get_first_super_admin()
514 c.first_admin = User.get_first_super_admin()
515 defaults = c.user.get_dict()
515 defaults = c.user.get_dict()
516
516
517 # Interim workaround if the user participated on any pull requests as a
517 # Interim workaround if the user participated on any pull requests as a
518 # reviewer.
518 # reviewer.
519 has_review = len(c.user.reviewer_pull_requests)
519 has_review = len(c.user.reviewer_pull_requests)
520 c.can_delete_user = not has_review
520 c.can_delete_user = not has_review
521 c.can_delete_user_message = ''
521 c.can_delete_user_message = ''
522 inactive_link = h.link_to(
522 inactive_link = h.link_to(
523 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
523 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
524 if has_review == 1:
524 if has_review == 1:
525 c.can_delete_user_message = h.literal(_(
525 c.can_delete_user_message = h.literal(_(
526 'The user participates as reviewer in {} pull request and '
526 'The user participates as reviewer in {} pull request and '
527 'cannot be deleted. \nYou can set the user to '
527 'cannot be deleted. \nYou can set the user to '
528 '"{}" instead of deleting it.').format(
528 '"{}" instead of deleting it.').format(
529 has_review, inactive_link))
529 has_review, inactive_link))
530 elif has_review:
530 elif has_review:
531 c.can_delete_user_message = h.literal(_(
531 c.can_delete_user_message = h.literal(_(
532 'The user participates as reviewer in {} pull requests and '
532 'The user participates as reviewer in {} pull requests and '
533 'cannot be deleted. \nYou can set the user to '
533 'cannot be deleted. \nYou can set the user to '
534 '"{}" instead of deleting it.').format(
534 '"{}" instead of deleting it.').format(
535 has_review, inactive_link))
535 has_review, inactive_link))
536
536
537 data = render(
537 data = render(
538 'rhodecode:templates/admin/users/user_edit.mako',
538 'rhodecode:templates/admin/users/user_edit.mako',
539 self._get_template_context(c), self.request)
539 self._get_template_context(c), self.request)
540 html = formencode.htmlfill.render(
540 html = formencode.htmlfill.render(
541 data,
541 data,
542 defaults=defaults,
542 defaults=defaults,
543 encoding="UTF-8",
543 encoding="UTF-8",
544 force_defaults=False
544 force_defaults=False
545 )
545 )
546 return Response(html)
546 return Response(html)
547
547
548 @LoginRequired()
548 @LoginRequired()
549 @HasPermissionAllDecorator('hg.admin')
549 @HasPermissionAllDecorator('hg.admin')
550 @view_config(
550 @view_config(
551 route_name='user_edit_global_perms', request_method='GET',
551 route_name='user_edit_global_perms', request_method='GET',
552 renderer='rhodecode:templates/admin/users/user_edit.mako')
552 renderer='rhodecode:templates/admin/users/user_edit.mako')
553 def user_edit_global_perms(self):
553 def user_edit_global_perms(self):
554 _ = self.request.translate
554 _ = self.request.translate
555 c = self.load_default_context()
555 c = self.load_default_context()
556 c.user = self.db_user
556 c.user = self.db_user
557
557
558 c.active = 'global_perms'
558 c.active = 'global_perms'
559
559
560 c.default_user = User.get_default_user()
560 c.default_user = User.get_default_user()
561 defaults = c.user.get_dict()
561 defaults = c.user.get_dict()
562 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
562 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
563 defaults.update(c.default_user.get_default_perms())
563 defaults.update(c.default_user.get_default_perms())
564 defaults.update(c.user.get_default_perms())
564 defaults.update(c.user.get_default_perms())
565
565
566 data = render(
566 data = render(
567 'rhodecode:templates/admin/users/user_edit.mako',
567 'rhodecode:templates/admin/users/user_edit.mako',
568 self._get_template_context(c), self.request)
568 self._get_template_context(c), self.request)
569 html = formencode.htmlfill.render(
569 html = formencode.htmlfill.render(
570 data,
570 data,
571 defaults=defaults,
571 defaults=defaults,
572 encoding="UTF-8",
572 encoding="UTF-8",
573 force_defaults=False
573 force_defaults=False
574 )
574 )
575 return Response(html)
575 return Response(html)
576
576
577 @LoginRequired()
577 @LoginRequired()
578 @HasPermissionAllDecorator('hg.admin')
578 @HasPermissionAllDecorator('hg.admin')
579 @CSRFRequired()
579 @CSRFRequired()
580 @view_config(
580 @view_config(
581 route_name='user_edit_global_perms_update', request_method='POST',
581 route_name='user_edit_global_perms_update', request_method='POST',
582 renderer='rhodecode:templates/admin/users/user_edit.mako')
582 renderer='rhodecode:templates/admin/users/user_edit.mako')
583 def user_edit_global_perms_update(self):
583 def user_edit_global_perms_update(self):
584 _ = self.request.translate
584 _ = self.request.translate
585 c = self.load_default_context()
585 c = self.load_default_context()
586
586
587 user_id = self.db_user_id
587 user_id = self.db_user_id
588 c.user = self.db_user
588 c.user = self.db_user
589
589
590 c.active = 'global_perms'
590 c.active = 'global_perms'
591 try:
591 try:
592 # first stage that verifies the checkbox
592 # first stage that verifies the checkbox
593 _form = UserIndividualPermissionsForm(self.request.translate)
593 _form = UserIndividualPermissionsForm(self.request.translate)
594 form_result = _form.to_python(dict(self.request.POST))
594 form_result = _form.to_python(dict(self.request.POST))
595 inherit_perms = form_result['inherit_default_permissions']
595 inherit_perms = form_result['inherit_default_permissions']
596 c.user.inherit_default_permissions = inherit_perms
596 c.user.inherit_default_permissions = inherit_perms
597 Session().add(c.user)
597 Session().add(c.user)
598
598
599 if not inherit_perms:
599 if not inherit_perms:
600 # only update the individual ones if we un check the flag
600 # only update the individual ones if we un check the flag
601 _form = UserPermissionsForm(
601 _form = UserPermissionsForm(
602 self.request.translate,
602 self.request.translate,
603 [x[0] for x in c.repo_create_choices],
603 [x[0] for x in c.repo_create_choices],
604 [x[0] for x in c.repo_create_on_write_choices],
604 [x[0] for x in c.repo_create_on_write_choices],
605 [x[0] for x in c.repo_group_create_choices],
605 [x[0] for x in c.repo_group_create_choices],
606 [x[0] for x in c.user_group_create_choices],
606 [x[0] for x in c.user_group_create_choices],
607 [x[0] for x in c.fork_choices],
607 [x[0] for x in c.fork_choices],
608 [x[0] for x in c.inherit_default_permission_choices])()
608 [x[0] for x in c.inherit_default_permission_choices])()
609
609
610 form_result = _form.to_python(dict(self.request.POST))
610 form_result = _form.to_python(dict(self.request.POST))
611 form_result.update({'perm_user_id': c.user.user_id})
611 form_result.update({'perm_user_id': c.user.user_id})
612
612
613 PermissionModel().update_user_permissions(form_result)
613 PermissionModel().update_user_permissions(form_result)
614
614
615 # TODO(marcink): implement global permissions
615 # TODO(marcink): implement global permissions
616 # audit_log.store_web('user.edit.permissions')
616 # audit_log.store_web('user.edit.permissions')
617
617
618 Session().commit()
618 Session().commit()
619
619
620 h.flash(_('User global permissions updated successfully'),
620 h.flash(_('User global permissions updated successfully'),
621 category='success')
621 category='success')
622
622
623 except formencode.Invalid as errors:
623 except formencode.Invalid as errors:
624 data = render(
624 data = render(
625 'rhodecode:templates/admin/users/user_edit.mako',
625 'rhodecode:templates/admin/users/user_edit.mako',
626 self._get_template_context(c), self.request)
626 self._get_template_context(c), self.request)
627 html = formencode.htmlfill.render(
627 html = formencode.htmlfill.render(
628 data,
628 data,
629 defaults=errors.value,
629 defaults=errors.value,
630 errors=errors.error_dict or {},
630 errors=errors.error_dict or {},
631 prefix_error=False,
631 prefix_error=False,
632 encoding="UTF-8",
632 encoding="UTF-8",
633 force_defaults=False
633 force_defaults=False
634 )
634 )
635 return Response(html)
635 return Response(html)
636 except Exception:
636 except Exception:
637 log.exception("Exception during permissions saving")
637 log.exception("Exception during permissions saving")
638 h.flash(_('An error occurred during permissions saving'),
638 h.flash(_('An error occurred during permissions saving'),
639 category='error')
639 category='error')
640
640
641 affected_user_ids = [user_id]
641 affected_user_ids = [user_id]
642 PermissionModel().trigger_permission_flush(affected_user_ids)
642 PermissionModel().trigger_permission_flush(affected_user_ids)
643 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
643 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
644
644
645 @LoginRequired()
645 @LoginRequired()
646 @HasPermissionAllDecorator('hg.admin')
646 @HasPermissionAllDecorator('hg.admin')
647 @CSRFRequired()
647 @CSRFRequired()
648 @view_config(
648 @view_config(
649 route_name='user_enable_force_password_reset', request_method='POST',
649 route_name='user_enable_force_password_reset', request_method='POST',
650 renderer='rhodecode:templates/admin/users/user_edit.mako')
650 renderer='rhodecode:templates/admin/users/user_edit.mako')
651 def user_enable_force_password_reset(self):
651 def user_enable_force_password_reset(self):
652 _ = self.request.translate
652 _ = self.request.translate
653 c = self.load_default_context()
653 c = self.load_default_context()
654
654
655 user_id = self.db_user_id
655 user_id = self.db_user_id
656 c.user = self.db_user
656 c.user = self.db_user
657
657
658 try:
658 try:
659 c.user.update_userdata(force_password_change=True)
659 c.user.update_userdata(force_password_change=True)
660
660
661 msg = _('Force password change enabled for user')
661 msg = _('Force password change enabled for user')
662 audit_logger.store_web('user.edit.password_reset.enabled',
662 audit_logger.store_web('user.edit.password_reset.enabled',
663 user=c.rhodecode_user)
663 user=c.rhodecode_user)
664
664
665 Session().commit()
665 Session().commit()
666 h.flash(msg, category='success')
666 h.flash(msg, category='success')
667 except Exception:
667 except Exception:
668 log.exception("Exception during password reset for user")
668 log.exception("Exception during password reset for user")
669 h.flash(_('An error occurred during password reset for user'),
669 h.flash(_('An error occurred during password reset for user'),
670 category='error')
670 category='error')
671
671
672 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
672 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
673
673
674 @LoginRequired()
674 @LoginRequired()
675 @HasPermissionAllDecorator('hg.admin')
675 @HasPermissionAllDecorator('hg.admin')
676 @CSRFRequired()
676 @CSRFRequired()
677 @view_config(
677 @view_config(
678 route_name='user_disable_force_password_reset', request_method='POST',
678 route_name='user_disable_force_password_reset', request_method='POST',
679 renderer='rhodecode:templates/admin/users/user_edit.mako')
679 renderer='rhodecode:templates/admin/users/user_edit.mako')
680 def user_disable_force_password_reset(self):
680 def user_disable_force_password_reset(self):
681 _ = self.request.translate
681 _ = self.request.translate
682 c = self.load_default_context()
682 c = self.load_default_context()
683
683
684 user_id = self.db_user_id
684 user_id = self.db_user_id
685 c.user = self.db_user
685 c.user = self.db_user
686
686
687 try:
687 try:
688 c.user.update_userdata(force_password_change=False)
688 c.user.update_userdata(force_password_change=False)
689
689
690 msg = _('Force password change disabled for user')
690 msg = _('Force password change disabled for user')
691 audit_logger.store_web(
691 audit_logger.store_web(
692 'user.edit.password_reset.disabled',
692 'user.edit.password_reset.disabled',
693 user=c.rhodecode_user)
693 user=c.rhodecode_user)
694
694
695 Session().commit()
695 Session().commit()
696 h.flash(msg, category='success')
696 h.flash(msg, category='success')
697 except Exception:
697 except Exception:
698 log.exception("Exception during password reset for user")
698 log.exception("Exception during password reset for user")
699 h.flash(_('An error occurred during password reset for user'),
699 h.flash(_('An error occurred during password reset for user'),
700 category='error')
700 category='error')
701
701
702 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
702 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
703
703
704 @LoginRequired()
704 @LoginRequired()
705 @HasPermissionAllDecorator('hg.admin')
705 @HasPermissionAllDecorator('hg.admin')
706 @CSRFRequired()
706 @CSRFRequired()
707 @view_config(
707 @view_config(
708 route_name='user_create_personal_repo_group', request_method='POST',
708 route_name='user_create_personal_repo_group', request_method='POST',
709 renderer='rhodecode:templates/admin/users/user_edit.mako')
709 renderer='rhodecode:templates/admin/users/user_edit.mako')
710 def user_create_personal_repo_group(self):
710 def user_create_personal_repo_group(self):
711 """
711 """
712 Create personal repository group for this user
712 Create personal repository group for this user
713 """
713 """
714 from rhodecode.model.repo_group import RepoGroupModel
714 from rhodecode.model.repo_group import RepoGroupModel
715
715
716 _ = self.request.translate
716 _ = self.request.translate
717 c = self.load_default_context()
717 c = self.load_default_context()
718
718
719 user_id = self.db_user_id
719 user_id = self.db_user_id
720 c.user = self.db_user
720 c.user = self.db_user
721
721
722 personal_repo_group = RepoGroup.get_user_personal_repo_group(
722 personal_repo_group = RepoGroup.get_user_personal_repo_group(
723 c.user.user_id)
723 c.user.user_id)
724 if personal_repo_group:
724 if personal_repo_group:
725 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
725 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
726
726
727 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
727 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
728 named_personal_group = RepoGroup.get_by_group_name(
728 named_personal_group = RepoGroup.get_by_group_name(
729 personal_repo_group_name)
729 personal_repo_group_name)
730 try:
730 try:
731
731
732 if named_personal_group and named_personal_group.user_id == c.user.user_id:
732 if named_personal_group and named_personal_group.user_id == c.user.user_id:
733 # migrate the same named group, and mark it as personal
733 # migrate the same named group, and mark it as personal
734 named_personal_group.personal = True
734 named_personal_group.personal = True
735 Session().add(named_personal_group)
735 Session().add(named_personal_group)
736 Session().commit()
736 Session().commit()
737 msg = _('Linked repository group `%s` as personal' % (
737 msg = _('Linked repository group `%s` as personal' % (
738 personal_repo_group_name,))
738 personal_repo_group_name,))
739 h.flash(msg, category='success')
739 h.flash(msg, category='success')
740 elif not named_personal_group:
740 elif not named_personal_group:
741 RepoGroupModel().create_personal_repo_group(c.user)
741 RepoGroupModel().create_personal_repo_group(c.user)
742
742
743 msg = _('Created repository group `%s`' % (
743 msg = _('Created repository group `%s`' % (
744 personal_repo_group_name,))
744 personal_repo_group_name,))
745 h.flash(msg, category='success')
745 h.flash(msg, category='success')
746 else:
746 else:
747 msg = _('Repository group `%s` is already taken' % (
747 msg = _('Repository group `%s` is already taken' % (
748 personal_repo_group_name,))
748 personal_repo_group_name,))
749 h.flash(msg, category='warning')
749 h.flash(msg, category='warning')
750 except Exception:
750 except Exception:
751 log.exception("Exception during repository group creation")
751 log.exception("Exception during repository group creation")
752 msg = _(
752 msg = _(
753 'An error occurred during repository group creation for user')
753 'An error occurred during repository group creation for user')
754 h.flash(msg, category='error')
754 h.flash(msg, category='error')
755 Session().rollback()
755 Session().rollback()
756
756
757 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
757 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
758
758
759 @LoginRequired()
759 @LoginRequired()
760 @HasPermissionAllDecorator('hg.admin')
760 @HasPermissionAllDecorator('hg.admin')
761 @view_config(
761 @view_config(
762 route_name='edit_user_auth_tokens', request_method='GET',
762 route_name='edit_user_auth_tokens', request_method='GET',
763 renderer='rhodecode:templates/admin/users/user_edit.mako')
763 renderer='rhodecode:templates/admin/users/user_edit.mako')
764 def auth_tokens(self):
764 def auth_tokens(self):
765 _ = self.request.translate
765 _ = self.request.translate
766 c = self.load_default_context()
766 c = self.load_default_context()
767 c.user = self.db_user
767 c.user = self.db_user
768
768
769 c.active = 'auth_tokens'
769 c.active = 'auth_tokens'
770
770
771 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
771 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
772 c.role_values = [
772 c.role_values = [
773 (x, AuthTokenModel.cls._get_role_name(x))
773 (x, AuthTokenModel.cls._get_role_name(x))
774 for x in AuthTokenModel.cls.ROLES]
774 for x in AuthTokenModel.cls.ROLES]
775 c.role_options = [(c.role_values, _("Role"))]
775 c.role_options = [(c.role_values, _("Role"))]
776 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
776 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
777 c.user.user_id, show_expired=True)
777 c.user.user_id, show_expired=True)
778 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
778 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
779 return self._get_template_context(c)
779 return self._get_template_context(c)
780
780
781 def maybe_attach_token_scope(self, token):
781 def maybe_attach_token_scope(self, token):
782 # implemented in EE edition
782 # implemented in EE edition
783 pass
783 pass
784
784
785 @LoginRequired()
785 @LoginRequired()
786 @HasPermissionAllDecorator('hg.admin')
786 @HasPermissionAllDecorator('hg.admin')
787 @CSRFRequired()
787 @CSRFRequired()
788 @view_config(
788 @view_config(
789 route_name='edit_user_auth_tokens_add', request_method='POST')
789 route_name='edit_user_auth_tokens_add', request_method='POST')
790 def auth_tokens_add(self):
790 def auth_tokens_add(self):
791 _ = self.request.translate
791 _ = self.request.translate
792 c = self.load_default_context()
792 c = self.load_default_context()
793
793
794 user_id = self.db_user_id
794 user_id = self.db_user_id
795 c.user = self.db_user
795 c.user = self.db_user
796
796
797 user_data = c.user.get_api_data()
797 user_data = c.user.get_api_data()
798 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
798 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
799 description = self.request.POST.get('description')
799 description = self.request.POST.get('description')
800 role = self.request.POST.get('role')
800 role = self.request.POST.get('role')
801
801
802 token = UserModel().add_auth_token(
802 token = UserModel().add_auth_token(
803 user=c.user.user_id,
803 user=c.user.user_id,
804 lifetime_minutes=lifetime, role=role, description=description,
804 lifetime_minutes=lifetime, role=role, description=description,
805 scope_callback=self.maybe_attach_token_scope)
805 scope_callback=self.maybe_attach_token_scope)
806 token_data = token.get_api_data()
806 token_data = token.get_api_data()
807
807
808 audit_logger.store_web(
808 audit_logger.store_web(
809 'user.edit.token.add', action_data={
809 'user.edit.token.add', action_data={
810 'data': {'token': token_data, 'user': user_data}},
810 'data': {'token': token_data, 'user': user_data}},
811 user=self._rhodecode_user, )
811 user=self._rhodecode_user, )
812 Session().commit()
812 Session().commit()
813
813
814 h.flash(_("Auth token successfully created"), category='success')
814 h.flash(_("Auth token successfully created"), category='success')
815 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
815 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
816
816
817 @LoginRequired()
817 @LoginRequired()
818 @HasPermissionAllDecorator('hg.admin')
818 @HasPermissionAllDecorator('hg.admin')
819 @CSRFRequired()
819 @CSRFRequired()
820 @view_config(
820 @view_config(
821 route_name='edit_user_auth_tokens_delete', request_method='POST')
821 route_name='edit_user_auth_tokens_delete', request_method='POST')
822 def auth_tokens_delete(self):
822 def auth_tokens_delete(self):
823 _ = self.request.translate
823 _ = self.request.translate
824 c = self.load_default_context()
824 c = self.load_default_context()
825
825
826 user_id = self.db_user_id
826 user_id = self.db_user_id
827 c.user = self.db_user
827 c.user = self.db_user
828
828
829 user_data = c.user.get_api_data()
829 user_data = c.user.get_api_data()
830
830
831 del_auth_token = self.request.POST.get('del_auth_token')
831 del_auth_token = self.request.POST.get('del_auth_token')
832
832
833 if del_auth_token:
833 if del_auth_token:
834 token = UserApiKeys.get_or_404(del_auth_token)
834 token = UserApiKeys.get_or_404(del_auth_token)
835 token_data = token.get_api_data()
835 token_data = token.get_api_data()
836
836
837 AuthTokenModel().delete(del_auth_token, c.user.user_id)
837 AuthTokenModel().delete(del_auth_token, c.user.user_id)
838 audit_logger.store_web(
838 audit_logger.store_web(
839 'user.edit.token.delete', action_data={
839 'user.edit.token.delete', action_data={
840 'data': {'token': token_data, 'user': user_data}},
840 'data': {'token': token_data, 'user': user_data}},
841 user=self._rhodecode_user,)
841 user=self._rhodecode_user,)
842 Session().commit()
842 Session().commit()
843 h.flash(_("Auth token successfully deleted"), category='success')
843 h.flash(_("Auth token successfully deleted"), category='success')
844
844
845 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
845 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
846
846
847 @LoginRequired()
847 @LoginRequired()
848 @HasPermissionAllDecorator('hg.admin')
848 @HasPermissionAllDecorator('hg.admin')
849 @view_config(
849 @view_config(
850 route_name='edit_user_ssh_keys', request_method='GET',
850 route_name='edit_user_ssh_keys', request_method='GET',
851 renderer='rhodecode:templates/admin/users/user_edit.mako')
851 renderer='rhodecode:templates/admin/users/user_edit.mako')
852 def ssh_keys(self):
852 def ssh_keys(self):
853 _ = self.request.translate
853 _ = self.request.translate
854 c = self.load_default_context()
854 c = self.load_default_context()
855 c.user = self.db_user
855 c.user = self.db_user
856
856
857 c.active = 'ssh_keys'
857 c.active = 'ssh_keys'
858 c.default_key = self.request.GET.get('default_key')
858 c.default_key = self.request.GET.get('default_key')
859 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
859 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
860 return self._get_template_context(c)
860 return self._get_template_context(c)
861
861
862 @LoginRequired()
862 @LoginRequired()
863 @HasPermissionAllDecorator('hg.admin')
863 @HasPermissionAllDecorator('hg.admin')
864 @view_config(
864 @view_config(
865 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
865 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
866 renderer='rhodecode:templates/admin/users/user_edit.mako')
866 renderer='rhodecode:templates/admin/users/user_edit.mako')
867 def ssh_keys_generate_keypair(self):
867 def ssh_keys_generate_keypair(self):
868 _ = self.request.translate
868 _ = self.request.translate
869 c = self.load_default_context()
869 c = self.load_default_context()
870
870
871 c.user = self.db_user
871 c.user = self.db_user
872
872
873 c.active = 'ssh_keys_generate'
873 c.active = 'ssh_keys_generate'
874 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
874 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
875 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
875 private_format = self.request.GET.get('private_format') \
876 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
877 c.private, c.public = SshKeyModel().generate_keypair(
878 comment=comment, private_format=private_format)
876
879
877 return self._get_template_context(c)
880 return self._get_template_context(c)
878
881
879 @LoginRequired()
882 @LoginRequired()
880 @HasPermissionAllDecorator('hg.admin')
883 @HasPermissionAllDecorator('hg.admin')
881 @CSRFRequired()
884 @CSRFRequired()
882 @view_config(
885 @view_config(
883 route_name='edit_user_ssh_keys_add', request_method='POST')
886 route_name='edit_user_ssh_keys_add', request_method='POST')
884 def ssh_keys_add(self):
887 def ssh_keys_add(self):
885 _ = self.request.translate
888 _ = self.request.translate
886 c = self.load_default_context()
889 c = self.load_default_context()
887
890
888 user_id = self.db_user_id
891 user_id = self.db_user_id
889 c.user = self.db_user
892 c.user = self.db_user
890
893
891 user_data = c.user.get_api_data()
894 user_data = c.user.get_api_data()
892 key_data = self.request.POST.get('key_data')
895 key_data = self.request.POST.get('key_data')
893 description = self.request.POST.get('description')
896 description = self.request.POST.get('description')
894
897
895 fingerprint = 'unknown'
898 fingerprint = 'unknown'
896 try:
899 try:
897 if not key_data:
900 if not key_data:
898 raise ValueError('Please add a valid public key')
901 raise ValueError('Please add a valid public key')
899
902
900 key = SshKeyModel().parse_key(key_data.strip())
903 key = SshKeyModel().parse_key(key_data.strip())
901 fingerprint = key.hash_md5()
904 fingerprint = key.hash_md5()
902
905
903 ssh_key = SshKeyModel().create(
906 ssh_key = SshKeyModel().create(
904 c.user.user_id, fingerprint, key.keydata, description)
907 c.user.user_id, fingerprint, key.keydata, description)
905 ssh_key_data = ssh_key.get_api_data()
908 ssh_key_data = ssh_key.get_api_data()
906
909
907 audit_logger.store_web(
910 audit_logger.store_web(
908 'user.edit.ssh_key.add', action_data={
911 'user.edit.ssh_key.add', action_data={
909 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
912 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
910 user=self._rhodecode_user, )
913 user=self._rhodecode_user, )
911 Session().commit()
914 Session().commit()
912
915
913 # Trigger an event on change of keys.
916 # Trigger an event on change of keys.
914 trigger(SshKeyFileChangeEvent(), self.request.registry)
917 trigger(SshKeyFileChangeEvent(), self.request.registry)
915
918
916 h.flash(_("Ssh Key successfully created"), category='success')
919 h.flash(_("Ssh Key successfully created"), category='success')
917
920
918 except IntegrityError:
921 except IntegrityError:
919 log.exception("Exception during ssh key saving")
922 log.exception("Exception during ssh key saving")
920 err = 'Such key with fingerprint `{}` already exists, ' \
923 err = 'Such key with fingerprint `{}` already exists, ' \
921 'please use a different one'.format(fingerprint)
924 'please use a different one'.format(fingerprint)
922 h.flash(_('An error occurred during ssh key saving: {}').format(err),
925 h.flash(_('An error occurred during ssh key saving: {}').format(err),
923 category='error')
926 category='error')
924 except Exception as e:
927 except Exception as e:
925 log.exception("Exception during ssh key saving")
928 log.exception("Exception during ssh key saving")
926 h.flash(_('An error occurred during ssh key saving: {}').format(e),
929 h.flash(_('An error occurred during ssh key saving: {}').format(e),
927 category='error')
930 category='error')
928
931
929 return HTTPFound(
932 return HTTPFound(
930 h.route_path('edit_user_ssh_keys', user_id=user_id))
933 h.route_path('edit_user_ssh_keys', user_id=user_id))
931
934
932 @LoginRequired()
935 @LoginRequired()
933 @HasPermissionAllDecorator('hg.admin')
936 @HasPermissionAllDecorator('hg.admin')
934 @CSRFRequired()
937 @CSRFRequired()
935 @view_config(
938 @view_config(
936 route_name='edit_user_ssh_keys_delete', request_method='POST')
939 route_name='edit_user_ssh_keys_delete', request_method='POST')
937 def ssh_keys_delete(self):
940 def ssh_keys_delete(self):
938 _ = self.request.translate
941 _ = self.request.translate
939 c = self.load_default_context()
942 c = self.load_default_context()
940
943
941 user_id = self.db_user_id
944 user_id = self.db_user_id
942 c.user = self.db_user
945 c.user = self.db_user
943
946
944 user_data = c.user.get_api_data()
947 user_data = c.user.get_api_data()
945
948
946 del_ssh_key = self.request.POST.get('del_ssh_key')
949 del_ssh_key = self.request.POST.get('del_ssh_key')
947
950
948 if del_ssh_key:
951 if del_ssh_key:
949 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
952 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
950 ssh_key_data = ssh_key.get_api_data()
953 ssh_key_data = ssh_key.get_api_data()
951
954
952 SshKeyModel().delete(del_ssh_key, c.user.user_id)
955 SshKeyModel().delete(del_ssh_key, c.user.user_id)
953 audit_logger.store_web(
956 audit_logger.store_web(
954 'user.edit.ssh_key.delete', action_data={
957 'user.edit.ssh_key.delete', action_data={
955 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
958 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
956 user=self._rhodecode_user,)
959 user=self._rhodecode_user,)
957 Session().commit()
960 Session().commit()
958 # Trigger an event on change of keys.
961 # Trigger an event on change of keys.
959 trigger(SshKeyFileChangeEvent(), self.request.registry)
962 trigger(SshKeyFileChangeEvent(), self.request.registry)
960 h.flash(_("Ssh key successfully deleted"), category='success')
963 h.flash(_("Ssh key successfully deleted"), category='success')
961
964
962 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
965 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
963
966
964 @LoginRequired()
967 @LoginRequired()
965 @HasPermissionAllDecorator('hg.admin')
968 @HasPermissionAllDecorator('hg.admin')
966 @view_config(
969 @view_config(
967 route_name='edit_user_emails', request_method='GET',
970 route_name='edit_user_emails', request_method='GET',
968 renderer='rhodecode:templates/admin/users/user_edit.mako')
971 renderer='rhodecode:templates/admin/users/user_edit.mako')
969 def emails(self):
972 def emails(self):
970 _ = self.request.translate
973 _ = self.request.translate
971 c = self.load_default_context()
974 c = self.load_default_context()
972 c.user = self.db_user
975 c.user = self.db_user
973
976
974 c.active = 'emails'
977 c.active = 'emails'
975 c.user_email_map = UserEmailMap.query() \
978 c.user_email_map = UserEmailMap.query() \
976 .filter(UserEmailMap.user == c.user).all()
979 .filter(UserEmailMap.user == c.user).all()
977
980
978 return self._get_template_context(c)
981 return self._get_template_context(c)
979
982
980 @LoginRequired()
983 @LoginRequired()
981 @HasPermissionAllDecorator('hg.admin')
984 @HasPermissionAllDecorator('hg.admin')
982 @CSRFRequired()
985 @CSRFRequired()
983 @view_config(
986 @view_config(
984 route_name='edit_user_emails_add', request_method='POST')
987 route_name='edit_user_emails_add', request_method='POST')
985 def emails_add(self):
988 def emails_add(self):
986 _ = self.request.translate
989 _ = self.request.translate
987 c = self.load_default_context()
990 c = self.load_default_context()
988
991
989 user_id = self.db_user_id
992 user_id = self.db_user_id
990 c.user = self.db_user
993 c.user = self.db_user
991
994
992 email = self.request.POST.get('new_email')
995 email = self.request.POST.get('new_email')
993 user_data = c.user.get_api_data()
996 user_data = c.user.get_api_data()
994 try:
997 try:
995
998
996 form = UserExtraEmailForm(self.request.translate)()
999 form = UserExtraEmailForm(self.request.translate)()
997 data = form.to_python({'email': email})
1000 data = form.to_python({'email': email})
998 email = data['email']
1001 email = data['email']
999
1002
1000 UserModel().add_extra_email(c.user.user_id, email)
1003 UserModel().add_extra_email(c.user.user_id, email)
1001 audit_logger.store_web(
1004 audit_logger.store_web(
1002 'user.edit.email.add',
1005 'user.edit.email.add',
1003 action_data={'email': email, 'user': user_data},
1006 action_data={'email': email, 'user': user_data},
1004 user=self._rhodecode_user)
1007 user=self._rhodecode_user)
1005 Session().commit()
1008 Session().commit()
1006 h.flash(_("Added new email address `%s` for user account") % email,
1009 h.flash(_("Added new email address `%s` for user account") % email,
1007 category='success')
1010 category='success')
1008 except formencode.Invalid as error:
1011 except formencode.Invalid as error:
1009 h.flash(h.escape(error.error_dict['email']), category='error')
1012 h.flash(h.escape(error.error_dict['email']), category='error')
1010 except IntegrityError:
1013 except IntegrityError:
1011 log.warning("Email %s already exists", email)
1014 log.warning("Email %s already exists", email)
1012 h.flash(_('Email `{}` is already registered for another user.').format(email),
1015 h.flash(_('Email `{}` is already registered for another user.').format(email),
1013 category='error')
1016 category='error')
1014 except Exception:
1017 except Exception:
1015 log.exception("Exception during email saving")
1018 log.exception("Exception during email saving")
1016 h.flash(_('An error occurred during email saving'),
1019 h.flash(_('An error occurred during email saving'),
1017 category='error')
1020 category='error')
1018 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1021 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1019
1022
1020 @LoginRequired()
1023 @LoginRequired()
1021 @HasPermissionAllDecorator('hg.admin')
1024 @HasPermissionAllDecorator('hg.admin')
1022 @CSRFRequired()
1025 @CSRFRequired()
1023 @view_config(
1026 @view_config(
1024 route_name='edit_user_emails_delete', request_method='POST')
1027 route_name='edit_user_emails_delete', request_method='POST')
1025 def emails_delete(self):
1028 def emails_delete(self):
1026 _ = self.request.translate
1029 _ = self.request.translate
1027 c = self.load_default_context()
1030 c = self.load_default_context()
1028
1031
1029 user_id = self.db_user_id
1032 user_id = self.db_user_id
1030 c.user = self.db_user
1033 c.user = self.db_user
1031
1034
1032 email_id = self.request.POST.get('del_email_id')
1035 email_id = self.request.POST.get('del_email_id')
1033 user_model = UserModel()
1036 user_model = UserModel()
1034
1037
1035 email = UserEmailMap.query().get(email_id).email
1038 email = UserEmailMap.query().get(email_id).email
1036 user_data = c.user.get_api_data()
1039 user_data = c.user.get_api_data()
1037 user_model.delete_extra_email(c.user.user_id, email_id)
1040 user_model.delete_extra_email(c.user.user_id, email_id)
1038 audit_logger.store_web(
1041 audit_logger.store_web(
1039 'user.edit.email.delete',
1042 'user.edit.email.delete',
1040 action_data={'email': email, 'user': user_data},
1043 action_data={'email': email, 'user': user_data},
1041 user=self._rhodecode_user)
1044 user=self._rhodecode_user)
1042 Session().commit()
1045 Session().commit()
1043 h.flash(_("Removed email address from user account"),
1046 h.flash(_("Removed email address from user account"),
1044 category='success')
1047 category='success')
1045 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1048 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1046
1049
1047 @LoginRequired()
1050 @LoginRequired()
1048 @HasPermissionAllDecorator('hg.admin')
1051 @HasPermissionAllDecorator('hg.admin')
1049 @view_config(
1052 @view_config(
1050 route_name='edit_user_ips', request_method='GET',
1053 route_name='edit_user_ips', request_method='GET',
1051 renderer='rhodecode:templates/admin/users/user_edit.mako')
1054 renderer='rhodecode:templates/admin/users/user_edit.mako')
1052 def ips(self):
1055 def ips(self):
1053 _ = self.request.translate
1056 _ = self.request.translate
1054 c = self.load_default_context()
1057 c = self.load_default_context()
1055 c.user = self.db_user
1058 c.user = self.db_user
1056
1059
1057 c.active = 'ips'
1060 c.active = 'ips'
1058 c.user_ip_map = UserIpMap.query() \
1061 c.user_ip_map = UserIpMap.query() \
1059 .filter(UserIpMap.user == c.user).all()
1062 .filter(UserIpMap.user == c.user).all()
1060
1063
1061 c.inherit_default_ips = c.user.inherit_default_permissions
1064 c.inherit_default_ips = c.user.inherit_default_permissions
1062 c.default_user_ip_map = UserIpMap.query() \
1065 c.default_user_ip_map = UserIpMap.query() \
1063 .filter(UserIpMap.user == User.get_default_user()).all()
1066 .filter(UserIpMap.user == User.get_default_user()).all()
1064
1067
1065 return self._get_template_context(c)
1068 return self._get_template_context(c)
1066
1069
1067 @LoginRequired()
1070 @LoginRequired()
1068 @HasPermissionAllDecorator('hg.admin')
1071 @HasPermissionAllDecorator('hg.admin')
1069 @CSRFRequired()
1072 @CSRFRequired()
1070 @view_config(
1073 @view_config(
1071 route_name='edit_user_ips_add', request_method='POST')
1074 route_name='edit_user_ips_add', request_method='POST')
1072 # NOTE(marcink): this view is allowed for default users, as we can
1075 # NOTE(marcink): this view is allowed for default users, as we can
1073 # edit their IP white list
1076 # edit their IP white list
1074 def ips_add(self):
1077 def ips_add(self):
1075 _ = self.request.translate
1078 _ = self.request.translate
1076 c = self.load_default_context()
1079 c = self.load_default_context()
1077
1080
1078 user_id = self.db_user_id
1081 user_id = self.db_user_id
1079 c.user = self.db_user
1082 c.user = self.db_user
1080
1083
1081 user_model = UserModel()
1084 user_model = UserModel()
1082 desc = self.request.POST.get('description')
1085 desc = self.request.POST.get('description')
1083 try:
1086 try:
1084 ip_list = user_model.parse_ip_range(
1087 ip_list = user_model.parse_ip_range(
1085 self.request.POST.get('new_ip'))
1088 self.request.POST.get('new_ip'))
1086 except Exception as e:
1089 except Exception as e:
1087 ip_list = []
1090 ip_list = []
1088 log.exception("Exception during ip saving")
1091 log.exception("Exception during ip saving")
1089 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1092 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1090 category='error')
1093 category='error')
1091 added = []
1094 added = []
1092 user_data = c.user.get_api_data()
1095 user_data = c.user.get_api_data()
1093 for ip in ip_list:
1096 for ip in ip_list:
1094 try:
1097 try:
1095 form = UserExtraIpForm(self.request.translate)()
1098 form = UserExtraIpForm(self.request.translate)()
1096 data = form.to_python({'ip': ip})
1099 data = form.to_python({'ip': ip})
1097 ip = data['ip']
1100 ip = data['ip']
1098
1101
1099 user_model.add_extra_ip(c.user.user_id, ip, desc)
1102 user_model.add_extra_ip(c.user.user_id, ip, desc)
1100 audit_logger.store_web(
1103 audit_logger.store_web(
1101 'user.edit.ip.add',
1104 'user.edit.ip.add',
1102 action_data={'ip': ip, 'user': user_data},
1105 action_data={'ip': ip, 'user': user_data},
1103 user=self._rhodecode_user)
1106 user=self._rhodecode_user)
1104 Session().commit()
1107 Session().commit()
1105 added.append(ip)
1108 added.append(ip)
1106 except formencode.Invalid as error:
1109 except formencode.Invalid as error:
1107 msg = error.error_dict['ip']
1110 msg = error.error_dict['ip']
1108 h.flash(msg, category='error')
1111 h.flash(msg, category='error')
1109 except Exception:
1112 except Exception:
1110 log.exception("Exception during ip saving")
1113 log.exception("Exception during ip saving")
1111 h.flash(_('An error occurred during ip saving'),
1114 h.flash(_('An error occurred during ip saving'),
1112 category='error')
1115 category='error')
1113 if added:
1116 if added:
1114 h.flash(
1117 h.flash(
1115 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1118 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1116 category='success')
1119 category='success')
1117 if 'default_user' in self.request.POST:
1120 if 'default_user' in self.request.POST:
1118 # case for editing global IP list we do it for 'DEFAULT' user
1121 # case for editing global IP list we do it for 'DEFAULT' user
1119 raise HTTPFound(h.route_path('admin_permissions_ips'))
1122 raise HTTPFound(h.route_path('admin_permissions_ips'))
1120 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1123 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1121
1124
1122 @LoginRequired()
1125 @LoginRequired()
1123 @HasPermissionAllDecorator('hg.admin')
1126 @HasPermissionAllDecorator('hg.admin')
1124 @CSRFRequired()
1127 @CSRFRequired()
1125 @view_config(
1128 @view_config(
1126 route_name='edit_user_ips_delete', request_method='POST')
1129 route_name='edit_user_ips_delete', request_method='POST')
1127 # NOTE(marcink): this view is allowed for default users, as we can
1130 # NOTE(marcink): this view is allowed for default users, as we can
1128 # edit their IP white list
1131 # edit their IP white list
1129 def ips_delete(self):
1132 def ips_delete(self):
1130 _ = self.request.translate
1133 _ = self.request.translate
1131 c = self.load_default_context()
1134 c = self.load_default_context()
1132
1135
1133 user_id = self.db_user_id
1136 user_id = self.db_user_id
1134 c.user = self.db_user
1137 c.user = self.db_user
1135
1138
1136 ip_id = self.request.POST.get('del_ip_id')
1139 ip_id = self.request.POST.get('del_ip_id')
1137 user_model = UserModel()
1140 user_model = UserModel()
1138 user_data = c.user.get_api_data()
1141 user_data = c.user.get_api_data()
1139 ip = UserIpMap.query().get(ip_id).ip_addr
1142 ip = UserIpMap.query().get(ip_id).ip_addr
1140 user_model.delete_extra_ip(c.user.user_id, ip_id)
1143 user_model.delete_extra_ip(c.user.user_id, ip_id)
1141 audit_logger.store_web(
1144 audit_logger.store_web(
1142 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1145 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1143 user=self._rhodecode_user)
1146 user=self._rhodecode_user)
1144 Session().commit()
1147 Session().commit()
1145 h.flash(_("Removed ip address from user whitelist"), category='success')
1148 h.flash(_("Removed ip address from user whitelist"), category='success')
1146
1149
1147 if 'default_user' in self.request.POST:
1150 if 'default_user' in self.request.POST:
1148 # case for editing global IP list we do it for 'DEFAULT' user
1151 # case for editing global IP list we do it for 'DEFAULT' user
1149 raise HTTPFound(h.route_path('admin_permissions_ips'))
1152 raise HTTPFound(h.route_path('admin_permissions_ips'))
1150 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1153 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1151
1154
1152 @LoginRequired()
1155 @LoginRequired()
1153 @HasPermissionAllDecorator('hg.admin')
1156 @HasPermissionAllDecorator('hg.admin')
1154 @view_config(
1157 @view_config(
1155 route_name='edit_user_groups_management', request_method='GET',
1158 route_name='edit_user_groups_management', request_method='GET',
1156 renderer='rhodecode:templates/admin/users/user_edit.mako')
1159 renderer='rhodecode:templates/admin/users/user_edit.mako')
1157 def groups_management(self):
1160 def groups_management(self):
1158 c = self.load_default_context()
1161 c = self.load_default_context()
1159 c.user = self.db_user
1162 c.user = self.db_user
1160 c.data = c.user.group_member
1163 c.data = c.user.group_member
1161
1164
1162 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1165 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1163 for group in c.user.group_member]
1166 for group in c.user.group_member]
1164 c.groups = json.dumps(groups)
1167 c.groups = json.dumps(groups)
1165 c.active = 'groups'
1168 c.active = 'groups'
1166
1169
1167 return self._get_template_context(c)
1170 return self._get_template_context(c)
1168
1171
1169 @LoginRequired()
1172 @LoginRequired()
1170 @HasPermissionAllDecorator('hg.admin')
1173 @HasPermissionAllDecorator('hg.admin')
1171 @CSRFRequired()
1174 @CSRFRequired()
1172 @view_config(
1175 @view_config(
1173 route_name='edit_user_groups_management_updates', request_method='POST')
1176 route_name='edit_user_groups_management_updates', request_method='POST')
1174 def groups_management_updates(self):
1177 def groups_management_updates(self):
1175 _ = self.request.translate
1178 _ = self.request.translate
1176 c = self.load_default_context()
1179 c = self.load_default_context()
1177
1180
1178 user_id = self.db_user_id
1181 user_id = self.db_user_id
1179 c.user = self.db_user
1182 c.user = self.db_user
1180
1183
1181 user_groups = set(self.request.POST.getall('users_group_id'))
1184 user_groups = set(self.request.POST.getall('users_group_id'))
1182 user_groups_objects = []
1185 user_groups_objects = []
1183
1186
1184 for ugid in user_groups:
1187 for ugid in user_groups:
1185 user_groups_objects.append(
1188 user_groups_objects.append(
1186 UserGroupModel().get_group(safe_int(ugid)))
1189 UserGroupModel().get_group(safe_int(ugid)))
1187 user_group_model = UserGroupModel()
1190 user_group_model = UserGroupModel()
1188 added_to_groups, removed_from_groups = \
1191 added_to_groups, removed_from_groups = \
1189 user_group_model.change_groups(c.user, user_groups_objects)
1192 user_group_model.change_groups(c.user, user_groups_objects)
1190
1193
1191 user_data = c.user.get_api_data()
1194 user_data = c.user.get_api_data()
1192 for user_group_id in added_to_groups:
1195 for user_group_id in added_to_groups:
1193 user_group = UserGroup.get(user_group_id)
1196 user_group = UserGroup.get(user_group_id)
1194 old_values = user_group.get_api_data()
1197 old_values = user_group.get_api_data()
1195 audit_logger.store_web(
1198 audit_logger.store_web(
1196 'user_group.edit.member.add',
1199 'user_group.edit.member.add',
1197 action_data={'user': user_data, 'old_data': old_values},
1200 action_data={'user': user_data, 'old_data': old_values},
1198 user=self._rhodecode_user)
1201 user=self._rhodecode_user)
1199
1202
1200 for user_group_id in removed_from_groups:
1203 for user_group_id in removed_from_groups:
1201 user_group = UserGroup.get(user_group_id)
1204 user_group = UserGroup.get(user_group_id)
1202 old_values = user_group.get_api_data()
1205 old_values = user_group.get_api_data()
1203 audit_logger.store_web(
1206 audit_logger.store_web(
1204 'user_group.edit.member.delete',
1207 'user_group.edit.member.delete',
1205 action_data={'user': user_data, 'old_data': old_values},
1208 action_data={'user': user_data, 'old_data': old_values},
1206 user=self._rhodecode_user)
1209 user=self._rhodecode_user)
1207
1210
1208 Session().commit()
1211 Session().commit()
1209 c.active = 'user_groups_management'
1212 c.active = 'user_groups_management'
1210 h.flash(_("Groups successfully changed"), category='success')
1213 h.flash(_("Groups successfully changed"), category='success')
1211
1214
1212 return HTTPFound(h.route_path(
1215 return HTTPFound(h.route_path(
1213 'edit_user_groups_management', user_id=user_id))
1216 'edit_user_groups_management', user_id=user_id))
1214
1217
1215 @LoginRequired()
1218 @LoginRequired()
1216 @HasPermissionAllDecorator('hg.admin')
1219 @HasPermissionAllDecorator('hg.admin')
1217 @view_config(
1220 @view_config(
1218 route_name='edit_user_audit_logs', request_method='GET',
1221 route_name='edit_user_audit_logs', request_method='GET',
1219 renderer='rhodecode:templates/admin/users/user_edit.mako')
1222 renderer='rhodecode:templates/admin/users/user_edit.mako')
1220 def user_audit_logs(self):
1223 def user_audit_logs(self):
1221 _ = self.request.translate
1224 _ = self.request.translate
1222 c = self.load_default_context()
1225 c = self.load_default_context()
1223 c.user = self.db_user
1226 c.user = self.db_user
1224
1227
1225 c.active = 'audit'
1228 c.active = 'audit'
1226
1229
1227 p = safe_int(self.request.GET.get('page', 1), 1)
1230 p = safe_int(self.request.GET.get('page', 1), 1)
1228
1231
1229 filter_term = self.request.GET.get('filter')
1232 filter_term = self.request.GET.get('filter')
1230 user_log = UserModel().get_user_log(c.user, filter_term)
1233 user_log = UserModel().get_user_log(c.user, filter_term)
1231
1234
1232 def url_generator(page_num):
1235 def url_generator(page_num):
1233 query_params = {
1236 query_params = {
1234 'page': page_num
1237 'page': page_num
1235 }
1238 }
1236 if filter_term:
1239 if filter_term:
1237 query_params['filter'] = filter_term
1240 query_params['filter'] = filter_term
1238 return self.request.current_route_path(_query=query_params)
1241 return self.request.current_route_path(_query=query_params)
1239
1242
1240 c.audit_logs = SqlPage(
1243 c.audit_logs = SqlPage(
1241 user_log, page=p, items_per_page=10, url_maker=url_generator)
1244 user_log, page=p, items_per_page=10, url_maker=url_generator)
1242 c.filter_term = filter_term
1245 c.filter_term = filter_term
1243 return self._get_template_context(c)
1246 return self._get_template_context(c)
1244
1247
1245 @LoginRequired()
1248 @LoginRequired()
1246 @HasPermissionAllDecorator('hg.admin')
1249 @HasPermissionAllDecorator('hg.admin')
1247 @view_config(
1250 @view_config(
1248 route_name='edit_user_audit_logs_download', request_method='GET',
1251 route_name='edit_user_audit_logs_download', request_method='GET',
1249 renderer='string')
1252 renderer='string')
1250 def user_audit_logs_download(self):
1253 def user_audit_logs_download(self):
1251 _ = self.request.translate
1254 _ = self.request.translate
1252 c = self.load_default_context()
1255 c = self.load_default_context()
1253 c.user = self.db_user
1256 c.user = self.db_user
1254
1257
1255 user_log = UserModel().get_user_log(c.user, filter_term=None)
1258 user_log = UserModel().get_user_log(c.user, filter_term=None)
1256
1259
1257 audit_log_data = {}
1260 audit_log_data = {}
1258 for entry in user_log:
1261 for entry in user_log:
1259 audit_log_data[entry.user_log_id] = entry.get_dict()
1262 audit_log_data[entry.user_log_id] = entry.get_dict()
1260
1263
1261 response = Response(json.dumps(audit_log_data, indent=4))
1264 response = Response(json.dumps(audit_log_data, indent=4))
1262 response.content_disposition = str(
1265 response.content_disposition = str(
1263 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1266 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1264 response.content_type = 'application/json'
1267 response.content_type = 'application/json'
1265
1268
1266 return response
1269 return response
1267
1270
1268 @LoginRequired()
1271 @LoginRequired()
1269 @HasPermissionAllDecorator('hg.admin')
1272 @HasPermissionAllDecorator('hg.admin')
1270 @view_config(
1273 @view_config(
1271 route_name='edit_user_perms_summary', request_method='GET',
1274 route_name='edit_user_perms_summary', request_method='GET',
1272 renderer='rhodecode:templates/admin/users/user_edit.mako')
1275 renderer='rhodecode:templates/admin/users/user_edit.mako')
1273 def user_perms_summary(self):
1276 def user_perms_summary(self):
1274 _ = self.request.translate
1277 _ = self.request.translate
1275 c = self.load_default_context()
1278 c = self.load_default_context()
1276 c.user = self.db_user
1279 c.user = self.db_user
1277
1280
1278 c.active = 'perms_summary'
1281 c.active = 'perms_summary'
1279 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1282 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1280
1283
1281 return self._get_template_context(c)
1284 return self._get_template_context(c)
1282
1285
1283 @LoginRequired()
1286 @LoginRequired()
1284 @HasPermissionAllDecorator('hg.admin')
1287 @HasPermissionAllDecorator('hg.admin')
1285 @view_config(
1288 @view_config(
1286 route_name='edit_user_perms_summary_json', request_method='GET',
1289 route_name='edit_user_perms_summary_json', request_method='GET',
1287 renderer='json_ext')
1290 renderer='json_ext')
1288 def user_perms_summary_json(self):
1291 def user_perms_summary_json(self):
1289 self.load_default_context()
1292 self.load_default_context()
1290 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1293 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1291
1294
1292 return perm_user.permissions
1295 return perm_user.permissions
1293
1296
1294 @LoginRequired()
1297 @LoginRequired()
1295 @HasPermissionAllDecorator('hg.admin')
1298 @HasPermissionAllDecorator('hg.admin')
1296 @view_config(
1299 @view_config(
1297 route_name='edit_user_caches', request_method='GET',
1300 route_name='edit_user_caches', request_method='GET',
1298 renderer='rhodecode:templates/admin/users/user_edit.mako')
1301 renderer='rhodecode:templates/admin/users/user_edit.mako')
1299 def user_caches(self):
1302 def user_caches(self):
1300 _ = self.request.translate
1303 _ = self.request.translate
1301 c = self.load_default_context()
1304 c = self.load_default_context()
1302 c.user = self.db_user
1305 c.user = self.db_user
1303
1306
1304 c.active = 'caches'
1307 c.active = 'caches'
1305 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1306
1309
1307 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1310 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1308 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1311 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1309 c.backend = c.region.backend
1312 c.backend = c.region.backend
1310 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1313 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1311
1314
1312 return self._get_template_context(c)
1315 return self._get_template_context(c)
1313
1316
1314 @LoginRequired()
1317 @LoginRequired()
1315 @HasPermissionAllDecorator('hg.admin')
1318 @HasPermissionAllDecorator('hg.admin')
1316 @CSRFRequired()
1319 @CSRFRequired()
1317 @view_config(
1320 @view_config(
1318 route_name='edit_user_caches_update', request_method='POST')
1321 route_name='edit_user_caches_update', request_method='POST')
1319 def user_caches_update(self):
1322 def user_caches_update(self):
1320 _ = self.request.translate
1323 _ = self.request.translate
1321 c = self.load_default_context()
1324 c = self.load_default_context()
1322 c.user = self.db_user
1325 c.user = self.db_user
1323
1326
1324 c.active = 'caches'
1327 c.active = 'caches'
1325 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1328 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1326
1329
1327 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1330 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1328 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1331 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1329
1332
1330 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1333 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1331
1334
1332 return HTTPFound(h.route_path(
1335 return HTTPFound(h.route_path(
1333 'edit_user_caches', user_id=c.user.user_id))
1336 'edit_user_caches', user_id=c.user.user_id))
@@ -1,174 +1,195 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import logging
20 import logging
21
21
22 from pyramid.view import view_config
22 from pyramid.view import view_config
23 from pyramid.response import FileResponse
23 from pyramid.response import FileResponse
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25
25
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.exceptions import (
28 from rhodecode.apps.file_store.exceptions import (
29 FileNotAllowedException, FileOverSizeException)
29 FileNotAllowedException, FileOverSizeException)
30
30
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 LoginRequired)
35 LoginRequired)
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.model.db import Session, FileStore, UserApiKeys
37 from rhodecode.model.db import Session, FileStore, UserApiKeys
37
38
38 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
39
40
40
41
41 class FileStoreView(BaseAppView):
42 class FileStoreView(BaseAppView):
42 upload_key = 'store_file'
43 upload_key = 'store_file'
43
44
44 def load_default_context(self):
45 def load_default_context(self):
45 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
46 self.storage = utils.get_file_storage(self.request.registry.settings)
47 self.storage = utils.get_file_storage(self.request.registry.settings)
47 return c
48 return c
48
49
50 def _guess_type(self, file_name):
51 """
52 Our own type guesser for mimetypes using the rich DB
53 """
54 if not hasattr(self, 'db'):
55 self.db = get_mimetypes_db()
56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
57 return _content_type, _encoding
58
49 def _serve_file(self, file_uid):
59 def _serve_file(self, file_uid):
50
60
51 if not self.storage.exists(file_uid):
61 if not self.storage.exists(file_uid):
52 store_path = self.storage.store_path(file_uid)
62 store_path = self.storage.store_path(file_uid)
53 log.debug('File with FID:%s not found in the store under `%s`',
63 log.debug('File with FID:%s not found in the store under `%s`',
54 file_uid, store_path)
64 file_uid, store_path)
55 raise HTTPNotFound()
65 raise HTTPNotFound()
56
66
57 db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
67 db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
58 if not db_obj:
68 if not db_obj:
59 raise HTTPNotFound()
69 raise HTTPNotFound()
60
70
61 # private upload for user
71 # private upload for user
62 if db_obj.check_acl and db_obj.scope_user_id:
72 if db_obj.check_acl and db_obj.scope_user_id:
63 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
73 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
64 db_obj.scope_user_id)
74 db_obj.scope_user_id)
65 user = db_obj.user
75 user = db_obj.user
66 if self._rhodecode_db_user.user_id != user.user_id:
76 if self._rhodecode_db_user.user_id != user.user_id:
67 log.warning('Access to file store object forbidden')
77 log.warning('Access to file store object forbidden')
68 raise HTTPNotFound()
78 raise HTTPNotFound()
69
79
70 # scoped to repository permissions
80 # scoped to repository permissions
71 if db_obj.check_acl and db_obj.scope_repo_id:
81 if db_obj.check_acl and db_obj.scope_repo_id:
72 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
82 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
73 db_obj.scope_repo_id)
83 db_obj.scope_repo_id)
74 repo = db_obj.repo
84 repo = db_obj.repo
75 perm_set = ['repository.read', 'repository.write', 'repository.admin']
85 perm_set = ['repository.read', 'repository.write', 'repository.admin']
76 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
86 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
77 if not has_perm:
87 if not has_perm:
78 log.warning('Access to file store object `%s` forbidden', file_uid)
88 log.warning('Access to file store object `%s` forbidden', file_uid)
79 raise HTTPNotFound()
89 raise HTTPNotFound()
80
90
81 # scoped to repository group permissions
91 # scoped to repository group permissions
82 if db_obj.check_acl and db_obj.scope_repo_group_id:
92 if db_obj.check_acl and db_obj.scope_repo_group_id:
83 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
93 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
84 db_obj.scope_repo_group_id)
94 db_obj.scope_repo_group_id)
85 repo_group = db_obj.repo_group
95 repo_group = db_obj.repo_group
86 perm_set = ['group.read', 'group.write', 'group.admin']
96 perm_set = ['group.read', 'group.write', 'group.admin']
87 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
97 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
88 if not has_perm:
98 if not has_perm:
89 log.warning('Access to file store object `%s` forbidden', file_uid)
99 log.warning('Access to file store object `%s` forbidden', file_uid)
90 raise HTTPNotFound()
100 raise HTTPNotFound()
91
101
92 FileStore.bump_access_counter(file_uid)
102 FileStore.bump_access_counter(file_uid)
93
103
94 file_path = self.storage.store_path(file_uid)
104 file_path = self.storage.store_path(file_uid)
95 return FileResponse(file_path)
105 content_type = 'application/octet-stream'
106 content_encoding = None
107
108 _content_type, _encoding = self._guess_type(file_path)
109 if _content_type:
110 content_type = _content_type
111
112 # For file store we don't submit any session data, this logic tells the
113 # Session lib to skip it
114 setattr(self.request, '_file_response', True)
115 return FileResponse(file_path, request=self.request,
116 content_type=content_type, content_encoding=content_encoding)
96
117
97 @LoginRequired()
118 @LoginRequired()
98 @NotAnonymous()
119 @NotAnonymous()
99 @CSRFRequired()
120 @CSRFRequired()
100 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
121 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
101 def upload_file(self):
122 def upload_file(self):
102 self.load_default_context()
123 self.load_default_context()
103 file_obj = self.request.POST.get(self.upload_key)
124 file_obj = self.request.POST.get(self.upload_key)
104
125
105 if file_obj is None:
126 if file_obj is None:
106 return {'store_fid': None,
127 return {'store_fid': None,
107 'access_path': None,
128 'access_path': None,
108 'error': '{} data field is missing'.format(self.upload_key)}
129 'error': '{} data field is missing'.format(self.upload_key)}
109
130
110 if not hasattr(file_obj, 'filename'):
131 if not hasattr(file_obj, 'filename'):
111 return {'store_fid': None,
132 return {'store_fid': None,
112 'access_path': None,
133 'access_path': None,
113 'error': 'filename cannot be read from the data field'}
134 'error': 'filename cannot be read from the data field'}
114
135
115 filename = file_obj.filename
136 filename = file_obj.filename
116
137
117 metadata = {
138 metadata = {
118 'user_uploaded': {'username': self._rhodecode_user.username,
139 'user_uploaded': {'username': self._rhodecode_user.username,
119 'user_id': self._rhodecode_user.user_id,
140 'user_id': self._rhodecode_user.user_id,
120 'ip': self._rhodecode_user.ip_addr}}
141 'ip': self._rhodecode_user.ip_addr}}
121 try:
142 try:
122 store_uid, metadata = self.storage.save_file(
143 store_uid, metadata = self.storage.save_file(
123 file_obj.file, filename, extra_metadata=metadata)
144 file_obj.file, filename, extra_metadata=metadata)
124 except FileNotAllowedException:
145 except FileNotAllowedException:
125 return {'store_fid': None,
146 return {'store_fid': None,
126 'access_path': None,
147 'access_path': None,
127 'error': 'File {} is not allowed.'.format(filename)}
148 'error': 'File {} is not allowed.'.format(filename)}
128
149
129 except FileOverSizeException:
150 except FileOverSizeException:
130 return {'store_fid': None,
151 return {'store_fid': None,
131 'access_path': None,
152 'access_path': None,
132 'error': 'File {} is exceeding allowed limit.'.format(filename)}
153 'error': 'File {} is exceeding allowed limit.'.format(filename)}
133
154
134 try:
155 try:
135 entry = FileStore.create(
156 entry = FileStore.create(
136 file_uid=store_uid, filename=metadata["filename"],
157 file_uid=store_uid, filename=metadata["filename"],
137 file_hash=metadata["sha256"], file_size=metadata["size"],
158 file_hash=metadata["sha256"], file_size=metadata["size"],
138 file_description=u'upload attachment',
159 file_description=u'upload attachment',
139 check_acl=False, user_id=self._rhodecode_user.user_id
160 check_acl=False, user_id=self._rhodecode_user.user_id
140 )
161 )
141 Session().add(entry)
162 Session().add(entry)
142 Session().commit()
163 Session().commit()
143 log.debug('Stored upload in DB as %s', entry)
164 log.debug('Stored upload in DB as %s', entry)
144 except Exception:
165 except Exception:
145 log.exception('Failed to store file %s', filename)
166 log.exception('Failed to store file %s', filename)
146 return {'store_fid': None,
167 return {'store_fid': None,
147 'access_path': None,
168 'access_path': None,
148 'error': 'File {} failed to store in DB.'.format(filename)}
169 'error': 'File {} failed to store in DB.'.format(filename)}
149
170
150 return {'store_fid': store_uid,
171 return {'store_fid': store_uid,
151 'access_path': h.route_path('download_file', fid=store_uid)}
172 'access_path': h.route_path('download_file', fid=store_uid)}
152
173
153 # ACL is checked by scopes, if no scope the file is accessible to all
174 # ACL is checked by scopes, if no scope the file is accessible to all
154 @view_config(route_name='download_file')
175 @view_config(route_name='download_file')
155 def download_file(self):
176 def download_file(self):
156 self.load_default_context()
177 self.load_default_context()
157 file_uid = self.request.matchdict['fid']
178 file_uid = self.request.matchdict['fid']
158 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
179 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
159 return self._serve_file(file_uid)
180 return self._serve_file(file_uid)
160
181
161 # in addition to @LoginRequired ACL is checked by scopes
182 # in addition to @LoginRequired ACL is checked by scopes
162 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
183 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
163 @NotAnonymous()
184 @NotAnonymous()
164 @view_config(route_name='download_file_by_token')
185 @view_config(route_name='download_file_by_token')
165 def download_file_by_token(self):
186 def download_file_by_token(self):
166 """
187 """
167 Special view that allows to access the download file by special URL that
188 Special view that allows to access the download file by special URL that
168 is stored inside the URL.
189 is stored inside the URL.
169
190
170 http://example.com/_file_store/token-download/TOKEN/FILE_UID
191 http://example.com/_file_store/token-download/TOKEN/FILE_UID
171 """
192 """
172 self.load_default_context()
193 self.load_default_context()
173 file_uid = self.request.matchdict['fid']
194 file_uid = self.request.matchdict['fid']
174 return self._serve_file(file_uid)
195 return self._serve_file(file_uid)
@@ -1,391 +1,391 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import User, Gist
25 from rhodecode.model.db import User, Gist
26 from rhodecode.model.gist import GistModel
26 from rhodecode.model.gist import GistModel
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 TestController, assert_session_flash)
30 TestController, assert_session_flash)
31
31
32
32
33 def route_path(name, params=None, **kwargs):
33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib
35 from rhodecode.apps._base import ADMIN_PREFIX
35 from rhodecode.apps._base import ADMIN_PREFIX
36
36
37 base_url = {
37 base_url = {
38 'gists_show': ADMIN_PREFIX + '/gists',
38 'gists_show': ADMIN_PREFIX + '/gists',
39 'gists_new': ADMIN_PREFIX + '/gists/new',
39 'gists_new': ADMIN_PREFIX + '/gists/new',
40 'gists_create': ADMIN_PREFIX + '/gists/create',
40 'gists_create': ADMIN_PREFIX + '/gists/create',
41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/{revision}',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/{revision}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}/{f_path}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}/{f_path}',
49
49
50 }[name].format(**kwargs)
50 }[name].format(**kwargs)
51
51
52 if params:
52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
54 return base_url
54 return base_url
55
55
56
56
57 class GistUtility(object):
57 class GistUtility(object):
58
58
59 def __init__(self):
59 def __init__(self):
60 self._gist_ids = []
60 self._gist_ids = []
61
61
62 def __call__(
62 def __call__(
63 self, f_name, content='some gist', lifetime=-1,
63 self, f_name, content='some gist', lifetime=-1,
64 description='gist-desc', gist_type='public',
64 description='gist-desc', gist_type='public',
65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
66 gist_mapping = {
66 gist_mapping = {
67 f_name: {'content': content}
67 f_name: {'content': content}
68 }
68 }
69 user = User.get_by_username(owner)
69 user = User.get_by_username(owner)
70 gist = GistModel().create(
70 gist = GistModel().create(
71 description, owner=user, gist_mapping=gist_mapping,
71 description, owner=user, gist_mapping=gist_mapping,
72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
73 Session().commit()
73 Session().commit()
74 self._gist_ids.append(gist.gist_id)
74 self._gist_ids.append(gist.gist_id)
75 return gist
75 return gist
76
76
77 def cleanup(self):
77 def cleanup(self):
78 for gist_id in self._gist_ids:
78 for gist_id in self._gist_ids:
79 gist = Gist.get(gist_id)
79 gist = Gist.get(gist_id)
80 if gist:
80 if gist:
81 Session().delete(gist)
81 Session().delete(gist)
82
82
83 Session().commit()
83 Session().commit()
84
84
85
85
86 @pytest.fixture()
86 @pytest.fixture()
87 def create_gist(request):
87 def create_gist(request):
88 gist_utility = GistUtility()
88 gist_utility = GistUtility()
89 request.addfinalizer(gist_utility.cleanup)
89 request.addfinalizer(gist_utility.cleanup)
90 return gist_utility
90 return gist_utility
91
91
92
92
93 class TestGistsController(TestController):
93 class TestGistsController(TestController):
94
94
95 def test_index_empty(self, create_gist):
95 def test_index_empty(self, create_gist):
96 self.log_user()
96 self.log_user()
97 response = self.app.get(route_path('gists_show'))
97 response = self.app.get(route_path('gists_show'))
98 response.mustcontain('data: [],')
98 response.mustcontain('data: [],')
99
99
100 def test_index(self, create_gist):
100 def test_index(self, create_gist):
101 self.log_user()
101 self.log_user()
102 g1 = create_gist('gist1')
102 g1 = create_gist('gist1')
103 g2 = create_gist('gist2', lifetime=1400)
103 g2 = create_gist('gist2', lifetime=1400)
104 g3 = create_gist('gist3', description='gist3-desc')
104 g3 = create_gist('gist3', description='gist3-desc')
105 g4 = create_gist('gist4', gist_type='private').gist_access_id
105 g4 = create_gist('gist4', gist_type='private').gist_access_id
106 response = self.app.get(route_path('gists_show'))
106 response = self.app.get(route_path('gists_show'))
107
107
108 response.mustcontain('gist: %s' % g1.gist_access_id)
108 response.mustcontain(g1.gist_access_id)
109 response.mustcontain('gist: %s' % g2.gist_access_id)
109 response.mustcontain(g2.gist_access_id)
110 response.mustcontain('gist: %s' % g3.gist_access_id)
110 response.mustcontain(g3.gist_access_id)
111 response.mustcontain('gist3-desc')
111 response.mustcontain('gist3-desc')
112 response.mustcontain(no=['gist: %s' % g4])
112 response.mustcontain(no=[g4])
113
113
114 # Expiration information should be visible
114 # Expiration information should be visible
115 expires_tag = '%s' % h.age_component(
115 expires_tag = '%s' % h.age_component(
116 h.time_to_utcdatetime(g2.gist_expires))
116 h.time_to_utcdatetime(g2.gist_expires))
117 response.mustcontain(expires_tag.replace('"', '\\"'))
117 response.mustcontain(expires_tag.replace('"', '\\"'))
118
118
119 def test_index_private_gists(self, create_gist):
119 def test_index_private_gists(self, create_gist):
120 self.log_user()
120 self.log_user()
121 gist = create_gist('gist5', gist_type='private')
121 gist = create_gist('gist5', gist_type='private')
122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
123
123
124 # and privates
124 # and privates
125 response.mustcontain('gist: %s' % gist.gist_access_id)
125 response.mustcontain(gist.gist_access_id)
126
126
127 def test_index_show_all(self, create_gist):
127 def test_index_show_all(self, create_gist):
128 self.log_user()
128 self.log_user()
129 create_gist('gist1')
129 create_gist('gist1')
130 create_gist('gist2', lifetime=1400)
130 create_gist('gist2', lifetime=1400)
131 create_gist('gist3', description='gist3-desc')
131 create_gist('gist3', description='gist3-desc')
132 create_gist('gist4', gist_type='private')
132 create_gist('gist4', gist_type='private')
133
133
134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
135
135
136 assert len(GistModel.get_all()) == 4
136 assert len(GistModel.get_all()) == 4
137 # and privates
137 # and privates
138 for gist in GistModel.get_all():
138 for gist in GistModel.get_all():
139 response.mustcontain('gist: %s' % gist.gist_access_id)
139 response.mustcontain(gist.gist_access_id)
140
140
141 def test_index_show_all_hidden_from_regular(self, create_gist):
141 def test_index_show_all_hidden_from_regular(self, create_gist):
142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
143 create_gist('gist2', gist_type='private')
143 create_gist('gist2', gist_type='private')
144 create_gist('gist3', gist_type='private')
144 create_gist('gist3', gist_type='private')
145 create_gist('gist4', gist_type='private')
145 create_gist('gist4', gist_type='private')
146
146
147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
148
148
149 assert len(GistModel.get_all()) == 3
149 assert len(GistModel.get_all()) == 3
150 # since we don't have access to private in this view, we
150 # since we don't have access to private in this view, we
151 # should see nothing
151 # should see nothing
152 for gist in GistModel.get_all():
152 for gist in GistModel.get_all():
153 response.mustcontain(no=['gist: %s' % gist.gist_access_id])
153 response.mustcontain(no=[gist.gist_access_id])
154
154
155 def test_create(self):
155 def test_create(self):
156 self.log_user()
156 self.log_user()
157 response = self.app.post(
157 response = self.app.post(
158 route_path('gists_create'),
158 route_path('gists_create'),
159 params={'lifetime': -1,
159 params={'lifetime': -1,
160 'content': 'gist test',
160 'content': 'gist test',
161 'filename': 'foo',
161 'filename': 'foo',
162 'gist_type': 'public',
162 'gist_type': 'public',
163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 'csrf_token': self.csrf_token},
164 'csrf_token': self.csrf_token},
165 status=302)
165 status=302)
166 response = response.follow()
166 response = response.follow()
167 response.mustcontain('added file: foo')
167 response.mustcontain('added file: foo')
168 response.mustcontain('gist test')
168 response.mustcontain('gist test')
169
169
170 def test_create_with_path_with_dirs(self):
170 def test_create_with_path_with_dirs(self):
171 self.log_user()
171 self.log_user()
172 response = self.app.post(
172 response = self.app.post(
173 route_path('gists_create'),
173 route_path('gists_create'),
174 params={'lifetime': -1,
174 params={'lifetime': -1,
175 'content': 'gist test',
175 'content': 'gist test',
176 'filename': '/home/foo',
176 'filename': '/home/foo',
177 'gist_type': 'public',
177 'gist_type': 'public',
178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
179 'csrf_token': self.csrf_token},
179 'csrf_token': self.csrf_token},
180 status=200)
180 status=200)
181 response.mustcontain('Filename /home/foo cannot be inside a directory')
181 response.mustcontain('Filename /home/foo cannot be inside a directory')
182
182
183 def test_access_expired_gist(self, create_gist):
183 def test_access_expired_gist(self, create_gist):
184 self.log_user()
184 self.log_user()
185 gist = create_gist('never-see-me')
185 gist = create_gist('never-see-me')
186 gist.gist_expires = 0 # 1970
186 gist.gist_expires = 0 # 1970
187 Session().add(gist)
187 Session().add(gist)
188 Session().commit()
188 Session().commit()
189
189
190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
191 status=404)
191 status=404)
192
192
193 def test_create_private(self):
193 def test_create_private(self):
194 self.log_user()
194 self.log_user()
195 response = self.app.post(
195 response = self.app.post(
196 route_path('gists_create'),
196 route_path('gists_create'),
197 params={'lifetime': -1,
197 params={'lifetime': -1,
198 'content': 'private gist test',
198 'content': 'private gist test',
199 'filename': 'private-foo',
199 'filename': 'private-foo',
200 'gist_type': 'private',
200 'gist_type': 'private',
201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
202 'csrf_token': self.csrf_token},
202 'csrf_token': self.csrf_token},
203 status=302)
203 status=302)
204 response = response.follow()
204 response = response.follow()
205 response.mustcontain('added file: private-foo<')
205 response.mustcontain('added file: private-foo<')
206 response.mustcontain('private gist test')
206 response.mustcontain('private gist test')
207 response.mustcontain('Private Gist')
207 response.mustcontain('Private Gist')
208 # Make sure private gists are not indexed by robots
208 # Make sure private gists are not indexed by robots
209 response.mustcontain(
209 response.mustcontain(
210 '<meta name="robots" content="noindex, nofollow">')
210 '<meta name="robots" content="noindex, nofollow">')
211
211
212 def test_create_private_acl_private(self):
212 def test_create_private_acl_private(self):
213 self.log_user()
213 self.log_user()
214 response = self.app.post(
214 response = self.app.post(
215 route_path('gists_create'),
215 route_path('gists_create'),
216 params={'lifetime': -1,
216 params={'lifetime': -1,
217 'content': 'private gist test',
217 'content': 'private gist test',
218 'filename': 'private-foo',
218 'filename': 'private-foo',
219 'gist_type': 'private',
219 'gist_type': 'private',
220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
221 'csrf_token': self.csrf_token},
221 'csrf_token': self.csrf_token},
222 status=302)
222 status=302)
223 response = response.follow()
223 response = response.follow()
224 response.mustcontain('added file: private-foo<')
224 response.mustcontain('added file: private-foo<')
225 response.mustcontain('private gist test')
225 response.mustcontain('private gist test')
226 response.mustcontain('Private Gist')
226 response.mustcontain('Private Gist')
227 # Make sure private gists are not indexed by robots
227 # Make sure private gists are not indexed by robots
228 response.mustcontain(
228 response.mustcontain(
229 '<meta name="robots" content="noindex, nofollow">')
229 '<meta name="robots" content="noindex, nofollow">')
230
230
231 def test_create_with_description(self):
231 def test_create_with_description(self):
232 self.log_user()
232 self.log_user()
233 response = self.app.post(
233 response = self.app.post(
234 route_path('gists_create'),
234 route_path('gists_create'),
235 params={'lifetime': -1,
235 params={'lifetime': -1,
236 'content': 'gist test',
236 'content': 'gist test',
237 'filename': 'foo-desc',
237 'filename': 'foo-desc',
238 'description': 'gist-desc',
238 'description': 'gist-desc',
239 'gist_type': 'public',
239 'gist_type': 'public',
240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
241 'csrf_token': self.csrf_token},
241 'csrf_token': self.csrf_token},
242 status=302)
242 status=302)
243 response = response.follow()
243 response = response.follow()
244 response.mustcontain('added file: foo-desc')
244 response.mustcontain('added file: foo-desc')
245 response.mustcontain('gist test')
245 response.mustcontain('gist test')
246 response.mustcontain('gist-desc')
246 response.mustcontain('gist-desc')
247
247
248 def test_create_public_with_anonymous_access(self):
248 def test_create_public_with_anonymous_access(self):
249 self.log_user()
249 self.log_user()
250 params = {
250 params = {
251 'lifetime': -1,
251 'lifetime': -1,
252 'content': 'gist test',
252 'content': 'gist test',
253 'filename': 'foo-desc',
253 'filename': 'foo-desc',
254 'description': 'gist-desc',
254 'description': 'gist-desc',
255 'gist_type': 'public',
255 'gist_type': 'public',
256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
257 'csrf_token': self.csrf_token
257 'csrf_token': self.csrf_token
258 }
258 }
259 response = self.app.post(
259 response = self.app.post(
260 route_path('gists_create'), params=params, status=302)
260 route_path('gists_create'), params=params, status=302)
261 self.logout_user()
261 self.logout_user()
262 response = response.follow()
262 response = response.follow()
263 response.mustcontain('added file: foo-desc')
263 response.mustcontain('added file: foo-desc')
264 response.mustcontain('gist test')
264 response.mustcontain('gist test')
265 response.mustcontain('gist-desc')
265 response.mustcontain('gist-desc')
266
266
267 def test_new(self):
267 def test_new(self):
268 self.log_user()
268 self.log_user()
269 self.app.get(route_path('gists_new'))
269 self.app.get(route_path('gists_new'))
270
270
271 def test_delete(self, create_gist):
271 def test_delete(self, create_gist):
272 self.log_user()
272 self.log_user()
273 gist = create_gist('delete-me')
273 gist = create_gist('delete-me')
274 response = self.app.post(
274 response = self.app.post(
275 route_path('gist_delete', gist_id=gist.gist_id),
275 route_path('gist_delete', gist_id=gist.gist_id),
276 params={'csrf_token': self.csrf_token})
276 params={'csrf_token': self.csrf_token})
277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
278
278
279 def test_delete_normal_user_his_gist(self, create_gist):
279 def test_delete_normal_user_his_gist(self, create_gist):
280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
282
282
283 response = self.app.post(
283 response = self.app.post(
284 route_path('gist_delete', gist_id=gist.gist_id),
284 route_path('gist_delete', gist_id=gist.gist_id),
285 params={'csrf_token': self.csrf_token})
285 params={'csrf_token': self.csrf_token})
286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
287
287
288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
290 gist = create_gist('delete-me-2')
290 gist = create_gist('delete-me-2')
291
291
292 self.app.post(
292 self.app.post(
293 route_path('gist_delete', gist_id=gist.gist_id),
293 route_path('gist_delete', gist_id=gist.gist_id),
294 params={'csrf_token': self.csrf_token}, status=404)
294 params={'csrf_token': self.csrf_token}, status=404)
295
295
296 def test_show(self, create_gist):
296 def test_show(self, create_gist):
297 gist = create_gist('gist-show-me')
297 gist = create_gist('gist-show-me')
298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
299
299
300 response.mustcontain('added file: gist-show-me<')
300 response.mustcontain('added file: gist-show-me<')
301
301
302 assert_response = response.assert_response()
302 assert_response = response.assert_response()
303 assert_response.element_equals_to(
303 assert_response.element_equals_to(
304 'div.rc-user span.user',
304 'div.rc-user span.user',
305 '<a href="/_profiles/test_admin">test_admin</a></span>')
305 '<a href="/_profiles/test_admin">test_admin</a></span>')
306
306
307 response.mustcontain('gist-desc')
307 response.mustcontain('gist-desc')
308
308
309 def test_show_without_hg(self, create_gist):
309 def test_show_without_hg(self, create_gist):
310 with mock.patch(
310 with mock.patch(
311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
312 gist = create_gist('gist-show-me-again')
312 gist = create_gist('gist-show-me-again')
313 self.app.get(
313 self.app.get(
314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
315
315
316 def test_show_acl_private(self, create_gist):
316 def test_show_acl_private(self, create_gist):
317 gist = create_gist('gist-show-me-only-when-im-logged-in',
317 gist = create_gist('gist-show-me-only-when-im-logged-in',
318 acl_level=Gist.ACL_LEVEL_PRIVATE)
318 acl_level=Gist.ACL_LEVEL_PRIVATE)
319 self.app.get(
319 self.app.get(
320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
321
321
322 # now we log-in we should see thi gist
322 # now we log-in we should see thi gist
323 self.log_user()
323 self.log_user()
324 response = self.app.get(
324 response = self.app.get(
325 route_path('gist_show', gist_id=gist.gist_access_id))
325 route_path('gist_show', gist_id=gist.gist_access_id))
326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
327
327
328 assert_response = response.assert_response()
328 assert_response = response.assert_response()
329 assert_response.element_equals_to(
329 assert_response.element_equals_to(
330 'div.rc-user span.user',
330 'div.rc-user span.user',
331 '<a href="/_profiles/test_admin">test_admin</a></span>')
331 '<a href="/_profiles/test_admin">test_admin</a></span>')
332 response.mustcontain('gist-desc')
332 response.mustcontain('gist-desc')
333
333
334 def test_show_as_raw(self, create_gist):
334 def test_show_as_raw(self, create_gist):
335 gist = create_gist('gist-show-me', content='GIST CONTENT')
335 gist = create_gist('gist-show-me', content='GIST CONTENT')
336 response = self.app.get(
336 response = self.app.get(
337 route_path('gist_show_formatted',
337 route_path('gist_show_formatted',
338 gist_id=gist.gist_access_id, revision='tip',
338 gist_id=gist.gist_access_id, revision='tip',
339 format='raw'))
339 format='raw'))
340 assert response.body == 'GIST CONTENT'
340 assert response.body == 'GIST CONTENT'
341
341
342 def test_show_as_raw_individual_file(self, create_gist):
342 def test_show_as_raw_individual_file(self, create_gist):
343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
344 response = self.app.get(
344 response = self.app.get(
345 route_path('gist_show_formatted_path',
345 route_path('gist_show_formatted_path',
346 gist_id=gist.gist_access_id, format='raw',
346 gist_id=gist.gist_access_id, format='raw',
347 revision='tip', f_path='gist-show-me-raw'))
347 revision='tip', f_path='gist-show-me-raw'))
348 assert response.body == 'GIST BODY'
348 assert response.body == 'GIST BODY'
349
349
350 def test_edit_page(self, create_gist):
350 def test_edit_page(self, create_gist):
351 self.log_user()
351 self.log_user()
352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
354 response.mustcontain('GIST EDIT BODY')
354 response.mustcontain('GIST EDIT BODY')
355
355
356 def test_edit_page_non_logged_user(self, create_gist):
356 def test_edit_page_non_logged_user(self, create_gist):
357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
359 status=302)
359 status=302)
360
360
361 def test_edit_normal_user_his_gist(self, create_gist):
361 def test_edit_normal_user_his_gist(self, create_gist):
362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
365 status=200))
365 status=200))
366
366
367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
369 gist = create_gist('delete-me')
369 gist = create_gist('delete-me')
370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
371 status=404)
371 status=404)
372
372
373 def test_user_first_name_is_escaped(self, user_util, create_gist):
373 def test_user_first_name_is_escaped(self, user_util, create_gist):
374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
376 password = 'test'
376 password = 'test'
377 user = user_util.create_user(
377 user = user_util.create_user(
378 firstname=xss_atack_string, password=password)
378 firstname=xss_atack_string, password=password)
379 create_gist('gist', gist_type='public', owner=user.username)
379 create_gist('gist', gist_type='public', owner=user.username)
380 response = self.app.get(route_path('gists_show'))
380 response = self.app.get(route_path('gists_show'))
381 response.mustcontain(xss_escaped_string)
381 response.mustcontain(xss_escaped_string)
382
382
383 def test_user_last_name_is_escaped(self, user_util, create_gist):
383 def test_user_last_name_is_escaped(self, user_util, create_gist):
384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
386 password = 'test'
386 password = 'test'
387 user = user_util.create_user(
387 user = user_util.create_user(
388 lastname=xss_atack_string, password=password)
388 lastname=xss_atack_string, password=password)
389 create_gist('gist', gist_type='public', owner=user.username)
389 create_gist('gist', gist_type='public', owner=user.username)
390 response = self.app.get(route_path('gists_show'))
390 response = self.app.get(route_path('gists_show'))
391 response.mustcontain(xss_escaped_string)
391 response.mustcontain(xss_escaped_string)
@@ -1,69 +1,69 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 def assert_and_get_main_filter_content(result):
22 def assert_and_get_main_filter_content(result):
23 repos = []
23 repos = []
24 groups = []
24 groups = []
25 commits = []
25 commits = []
26 users = []
26 users = []
27 for data_item in result:
27 for data_item in result:
28 assert data_item['id']
28 assert data_item['id']
29 assert data_item['value']
29 assert data_item['value']
30 assert data_item['value_display']
30 assert data_item['value_display']
31 assert data_item['url']
31 assert data_item['url']
32
32
33 if data_item['type'] == 'search':
33 if data_item['type'] == 'search':
34 display_val = data_item['value_display']
34 display_val = data_item['value_display']
35 if data_item['id'] == -1:
35 if data_item['id'] == -1:
36 assert 'File search for:' in display_val, display_val
36 assert 'File content search for:' in display_val, display_val
37 elif data_item['id'] == -2:
37 elif data_item['id'] == -2:
38 assert 'Commit search for:' in display_val, display_val
38 assert 'Commit search for:' in display_val, display_val
39 else:
39 else:
40 assert False, 'No Proper ID returned {}'.format(data_item['id'])
40 assert False, 'No Proper ID returned {}'.format(data_item['id'])
41
41
42 elif data_item['type'] == 'repo':
42 elif data_item['type'] == 'repo':
43 repos.append(data_item)
43 repos.append(data_item)
44 elif data_item['type'] == 'repo_group':
44 elif data_item['type'] == 'repo_group':
45 groups.append(data_item)
45 groups.append(data_item)
46 elif data_item['type'] == 'user':
46 elif data_item['type'] == 'user':
47 users.append(data_item)
47 users.append(data_item)
48 elif data_item['type'] == 'commit':
48 elif data_item['type'] == 'commit':
49 commits.append(data_item)
49 commits.append(data_item)
50 else:
50 else:
51 raise Exception('invalid type `%s`' % data_item['type'])
51 raise Exception('invalid type `%s`' % data_item['type'])
52
52
53 return repos, groups, users, commits
53 return repos, groups, users, commits
54
54
55
55
56 def assert_and_get_repo_list_content(result):
56 def assert_and_get_repo_list_content(result):
57 repos = []
57 repos = []
58 for data in result:
58 for data in result:
59 for data_item in data['children']:
59 for data_item in data['children']:
60 assert data_item['id']
60 assert data_item['id']
61 assert data_item['text']
61 assert data_item['text']
62 assert data_item['url']
62 assert data_item['url']
63
63
64 if data_item['type'] == 'repo':
64 if data_item['type'] == 'repo':
65 repos.append(data_item)
65 repos.append(data_item)
66 else:
66 else:
67 raise Exception('invalid type %s' % data_item['type'])
67 raise Exception('invalid type %s' % data_item['type'])
68
68
69 return repos
69 return repos
@@ -1,830 +1,830 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound
25 from pyramid.httpexceptions import HTTPNotFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27
27
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 HasRepoGroupPermissionAny, AuthUser)
32 HasRepoGroupPermissionAny, AuthUser)
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 from rhodecode.lib.index import searcher_from_config
34 from rhodecode.lib.index import searcher_from_config
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
36 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 func, true, or_, case, in_filter_generator, Session,
38 func, true, or_, case, in_filter_generator, Session,
39 Repository, RepoGroup, User, UserGroup)
39 Repository, RepoGroup, User, UserGroup)
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.repo_group import RepoGroupModel
41 from rhodecode.model.repo_group import RepoGroupModel
42 from rhodecode.model.user import UserModel
42 from rhodecode.model.user import UserModel
43 from rhodecode.model.user_group import UserGroupModel
43 from rhodecode.model.user_group import UserGroupModel
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class HomeView(BaseAppView, DataGridAppView):
48 class HomeView(BaseAppView, DataGridAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 c = self._get_local_tmpl_context()
51 c = self._get_local_tmpl_context()
52 c.user = c.auth_user.get_instance()
52 c.user = c.auth_user.get_instance()
53
53
54 return c
54 return c
55
55
56 @LoginRequired()
56 @LoginRequired()
57 @view_config(
57 @view_config(
58 route_name='user_autocomplete_data', request_method='GET',
58 route_name='user_autocomplete_data', request_method='GET',
59 renderer='json_ext', xhr=True)
59 renderer='json_ext', xhr=True)
60 def user_autocomplete_data(self):
60 def user_autocomplete_data(self):
61 self.load_default_context()
61 self.load_default_context()
62 query = self.request.GET.get('query')
62 query = self.request.GET.get('query')
63 active = str2bool(self.request.GET.get('active') or True)
63 active = str2bool(self.request.GET.get('active') or True)
64 include_groups = str2bool(self.request.GET.get('user_groups'))
64 include_groups = str2bool(self.request.GET.get('user_groups'))
65 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
65 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
66 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
66 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
67
67
68 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
68 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
69 query, active, include_groups)
69 query, active, include_groups)
70
70
71 _users = UserModel().get_users(
71 _users = UserModel().get_users(
72 name_contains=query, only_active=active)
72 name_contains=query, only_active=active)
73
73
74 def maybe_skip_default_user(usr):
74 def maybe_skip_default_user(usr):
75 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
75 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
76 return False
76 return False
77 return True
77 return True
78 _users = filter(maybe_skip_default_user, _users)
78 _users = filter(maybe_skip_default_user, _users)
79
79
80 if include_groups:
80 if include_groups:
81 # extend with user groups
81 # extend with user groups
82 _user_groups = UserGroupModel().get_user_groups(
82 _user_groups = UserGroupModel().get_user_groups(
83 name_contains=query, only_active=active,
83 name_contains=query, only_active=active,
84 expand_groups=expand_groups)
84 expand_groups=expand_groups)
85 _users = _users + _user_groups
85 _users = _users + _user_groups
86
86
87 return {'suggestions': _users}
87 return {'suggestions': _users}
88
88
89 @LoginRequired()
89 @LoginRequired()
90 @NotAnonymous()
90 @NotAnonymous()
91 @view_config(
91 @view_config(
92 route_name='user_group_autocomplete_data', request_method='GET',
92 route_name='user_group_autocomplete_data', request_method='GET',
93 renderer='json_ext', xhr=True)
93 renderer='json_ext', xhr=True)
94 def user_group_autocomplete_data(self):
94 def user_group_autocomplete_data(self):
95 self.load_default_context()
95 self.load_default_context()
96 query = self.request.GET.get('query')
96 query = self.request.GET.get('query')
97 active = str2bool(self.request.GET.get('active') or True)
97 active = str2bool(self.request.GET.get('active') or True)
98 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
98 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
99
99
100 log.debug('generating user group list, query:%s, active:%s',
100 log.debug('generating user group list, query:%s, active:%s',
101 query, active)
101 query, active)
102
102
103 _user_groups = UserGroupModel().get_user_groups(
103 _user_groups = UserGroupModel().get_user_groups(
104 name_contains=query, only_active=active,
104 name_contains=query, only_active=active,
105 expand_groups=expand_groups)
105 expand_groups=expand_groups)
106 _user_groups = _user_groups
106 _user_groups = _user_groups
107
107
108 return {'suggestions': _user_groups}
108 return {'suggestions': _user_groups}
109
109
110 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
110 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
111 org_query = name_contains
111 org_query = name_contains
112 allowed_ids = self._rhodecode_user.repo_acl_ids(
112 allowed_ids = self._rhodecode_user.repo_acl_ids(
113 ['repository.read', 'repository.write', 'repository.admin'],
113 ['repository.read', 'repository.write', 'repository.admin'],
114 cache=False, name_filter=name_contains) or [-1]
114 cache=False, name_filter=name_contains) or [-1]
115
115
116 query = Session().query(
116 query = Session().query(
117 Repository.repo_name,
117 Repository.repo_name,
118 Repository.repo_id,
118 Repository.repo_id,
119 Repository.repo_type,
119 Repository.repo_type,
120 Repository.private,
120 Repository.private,
121 )\
121 )\
122 .filter(Repository.archived.isnot(true()))\
122 .filter(Repository.archived.isnot(true()))\
123 .filter(or_(
123 .filter(or_(
124 # generate multiple IN to fix limitation problems
124 # generate multiple IN to fix limitation problems
125 *in_filter_generator(Repository.repo_id, allowed_ids)
125 *in_filter_generator(Repository.repo_id, allowed_ids)
126 ))
126 ))
127
127
128 query = query.order_by(case(
128 query = query.order_by(case(
129 [
129 [
130 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
130 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
131 ],
131 ],
132 ))
132 ))
133 query = query.order_by(func.length(Repository.repo_name))
133 query = query.order_by(func.length(Repository.repo_name))
134 query = query.order_by(Repository.repo_name)
134 query = query.order_by(Repository.repo_name)
135
135
136 if repo_type:
136 if repo_type:
137 query = query.filter(Repository.repo_type == repo_type)
137 query = query.filter(Repository.repo_type == repo_type)
138
138
139 if name_contains:
139 if name_contains:
140 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
140 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
141 query = query.filter(
141 query = query.filter(
142 Repository.repo_name.ilike(ilike_expression))
142 Repository.repo_name.ilike(ilike_expression))
143 query = query.limit(limit)
143 query = query.limit(limit)
144
144
145 acl_iter = query
145 acl_iter = query
146
146
147 return [
147 return [
148 {
148 {
149 'id': obj.repo_name,
149 'id': obj.repo_name,
150 'value': org_query,
150 'value': org_query,
151 'value_display': obj.repo_name,
151 'value_display': obj.repo_name,
152 'text': obj.repo_name,
152 'text': obj.repo_name,
153 'type': 'repo',
153 'type': 'repo',
154 'repo_id': obj.repo_id,
154 'repo_id': obj.repo_id,
155 'repo_type': obj.repo_type,
155 'repo_type': obj.repo_type,
156 'private': obj.private,
156 'private': obj.private,
157 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
157 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
158 }
158 }
159 for obj in acl_iter]
159 for obj in acl_iter]
160
160
161 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
161 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
162 org_query = name_contains
162 org_query = name_contains
163 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
163 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
164 ['group.read', 'group.write', 'group.admin'],
164 ['group.read', 'group.write', 'group.admin'],
165 cache=False, name_filter=name_contains) or [-1]
165 cache=False, name_filter=name_contains) or [-1]
166
166
167 query = Session().query(
167 query = Session().query(
168 RepoGroup.group_id,
168 RepoGroup.group_id,
169 RepoGroup.group_name,
169 RepoGroup.group_name,
170 )\
170 )\
171 .filter(or_(
171 .filter(or_(
172 # generate multiple IN to fix limitation problems
172 # generate multiple IN to fix limitation problems
173 *in_filter_generator(RepoGroup.group_id, allowed_ids)
173 *in_filter_generator(RepoGroup.group_id, allowed_ids)
174 ))
174 ))
175
175
176 query = query.order_by(case(
176 query = query.order_by(case(
177 [
177 [
178 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
178 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
179 ],
179 ],
180 ))
180 ))
181 query = query.order_by(func.length(RepoGroup.group_name))
181 query = query.order_by(func.length(RepoGroup.group_name))
182 query = query.order_by(RepoGroup.group_name)
182 query = query.order_by(RepoGroup.group_name)
183
183
184 if name_contains:
184 if name_contains:
185 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
185 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
186 query = query.filter(
186 query = query.filter(
187 RepoGroup.group_name.ilike(ilike_expression))
187 RepoGroup.group_name.ilike(ilike_expression))
188 query = query.limit(limit)
188 query = query.limit(limit)
189
189
190 acl_iter = query
190 acl_iter = query
191
191
192 return [
192 return [
193 {
193 {
194 'id': obj.group_name,
194 'id': obj.group_name,
195 'value': org_query,
195 'value': org_query,
196 'value_display': obj.group_name,
196 'value_display': obj.group_name,
197 'text': obj.group_name,
197 'text': obj.group_name,
198 'type': 'repo_group',
198 'type': 'repo_group',
199 'repo_group_id': obj.group_id,
199 'repo_group_id': obj.group_id,
200 'url': h.route_path(
200 'url': h.route_path(
201 'repo_group_home', repo_group_name=obj.group_name)
201 'repo_group_home', repo_group_name=obj.group_name)
202 }
202 }
203 for obj in acl_iter]
203 for obj in acl_iter]
204
204
205 def _get_user_list(self, name_contains=None, limit=20):
205 def _get_user_list(self, name_contains=None, limit=20):
206 org_query = name_contains
206 org_query = name_contains
207 if not name_contains:
207 if not name_contains:
208 return [], False
208 return [], False
209
209
210 # TODO(marcink): should all logged in users be allowed to search others?
210 # TODO(marcink): should all logged in users be allowed to search others?
211 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
211 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
212 if not allowed_user_search:
212 if not allowed_user_search:
213 return [], False
213 return [], False
214
214
215 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
215 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
216 if len(name_contains) != 1:
216 if len(name_contains) != 1:
217 return [], False
217 return [], False
218
218
219 name_contains = name_contains[0]
219 name_contains = name_contains[0]
220
220
221 query = User.query()\
221 query = User.query()\
222 .order_by(func.length(User.username))\
222 .order_by(func.length(User.username))\
223 .order_by(User.username) \
223 .order_by(User.username) \
224 .filter(User.username != User.DEFAULT_USER)
224 .filter(User.username != User.DEFAULT_USER)
225
225
226 if name_contains:
226 if name_contains:
227 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
227 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
228 query = query.filter(
228 query = query.filter(
229 User.username.ilike(ilike_expression))
229 User.username.ilike(ilike_expression))
230 query = query.limit(limit)
230 query = query.limit(limit)
231
231
232 acl_iter = query
232 acl_iter = query
233
233
234 return [
234 return [
235 {
235 {
236 'id': obj.user_id,
236 'id': obj.user_id,
237 'value': org_query,
237 'value': org_query,
238 'value_display': 'user: `{}`'.format(obj.username),
238 'value_display': 'user: `{}`'.format(obj.username),
239 'type': 'user',
239 'type': 'user',
240 'icon_link': h.gravatar_url(obj.email, 30),
240 'icon_link': h.gravatar_url(obj.email, 30),
241 'url': h.route_path(
241 'url': h.route_path(
242 'user_profile', username=obj.username)
242 'user_profile', username=obj.username)
243 }
243 }
244 for obj in acl_iter], True
244 for obj in acl_iter], True
245
245
246 def _get_user_groups_list(self, name_contains=None, limit=20):
246 def _get_user_groups_list(self, name_contains=None, limit=20):
247 org_query = name_contains
247 org_query = name_contains
248 if not name_contains:
248 if not name_contains:
249 return [], False
249 return [], False
250
250
251 # TODO(marcink): should all logged in users be allowed to search others?
251 # TODO(marcink): should all logged in users be allowed to search others?
252 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
252 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
253 if not allowed_user_search:
253 if not allowed_user_search:
254 return [], False
254 return [], False
255
255
256 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
256 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
257 if len(name_contains) != 1:
257 if len(name_contains) != 1:
258 return [], False
258 return [], False
259
259
260 name_contains = name_contains[0]
260 name_contains = name_contains[0]
261
261
262 query = UserGroup.query()\
262 query = UserGroup.query()\
263 .order_by(func.length(UserGroup.users_group_name))\
263 .order_by(func.length(UserGroup.users_group_name))\
264 .order_by(UserGroup.users_group_name)
264 .order_by(UserGroup.users_group_name)
265
265
266 if name_contains:
266 if name_contains:
267 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
267 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
268 query = query.filter(
268 query = query.filter(
269 UserGroup.users_group_name.ilike(ilike_expression))
269 UserGroup.users_group_name.ilike(ilike_expression))
270 query = query.limit(limit)
270 query = query.limit(limit)
271
271
272 acl_iter = query
272 acl_iter = query
273
273
274 return [
274 return [
275 {
275 {
276 'id': obj.users_group_id,
276 'id': obj.users_group_id,
277 'value': org_query,
277 'value': org_query,
278 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
278 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
279 'type': 'user_group',
279 'type': 'user_group',
280 'url': h.route_path(
280 'url': h.route_path(
281 'user_group_profile', user_group_name=obj.users_group_name)
281 'user_group_profile', user_group_name=obj.users_group_name)
282 }
282 }
283 for obj in acl_iter], True
283 for obj in acl_iter], True
284
284
285 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
285 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
286 repo_name = repo_group_name = None
286 repo_name = repo_group_name = None
287 if repo:
287 if repo:
288 repo_name = repo.repo_name
288 repo_name = repo.repo_name
289 if repo_group:
289 if repo_group:
290 repo_group_name = repo_group.group_name
290 repo_group_name = repo_group.group_name
291
291
292 org_query = query
292 org_query = query
293 if not query or len(query) < 3 or not searcher:
293 if not query or len(query) < 3 or not searcher:
294 return [], False
294 return [], False
295
295
296 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
296 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
297
297
298 if len(commit_hashes) != 1:
298 if len(commit_hashes) != 1:
299 return [], False
299 return [], False
300
300
301 commit_hash = commit_hashes[0]
301 commit_hash = commit_hashes[0]
302
302
303 result = searcher.search(
303 result = searcher.search(
304 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
304 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
305 repo_name, repo_group_name, raise_on_exc=False)
305 repo_name, repo_group_name, raise_on_exc=False)
306
306
307 commits = []
307 commits = []
308 for entry in result['results']:
308 for entry in result['results']:
309 repo_data = {
309 repo_data = {
310 'repository_id': entry.get('repository_id'),
310 'repository_id': entry.get('repository_id'),
311 'repository_type': entry.get('repo_type'),
311 'repository_type': entry.get('repo_type'),
312 'repository_name': entry.get('repository'),
312 'repository_name': entry.get('repository'),
313 }
313 }
314
314
315 commit_entry = {
315 commit_entry = {
316 'id': entry['commit_id'],
316 'id': entry['commit_id'],
317 'value': org_query,
317 'value': org_query,
318 'value_display': '`{}` commit: {}'.format(
318 'value_display': '`{}` commit: {}'.format(
319 entry['repository'], entry['commit_id']),
319 entry['repository'], entry['commit_id']),
320 'type': 'commit',
320 'type': 'commit',
321 'repo': entry['repository'],
321 'repo': entry['repository'],
322 'repo_data': repo_data,
322 'repo_data': repo_data,
323
323
324 'url': h.route_path(
324 'url': h.route_path(
325 'repo_commit',
325 'repo_commit',
326 repo_name=entry['repository'], commit_id=entry['commit_id'])
326 repo_name=entry['repository'], commit_id=entry['commit_id'])
327 }
327 }
328
328
329 commits.append(commit_entry)
329 commits.append(commit_entry)
330 return commits, True
330 return commits, True
331
331
332 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
332 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
333 repo_name = repo_group_name = None
333 repo_name = repo_group_name = None
334 if repo:
334 if repo:
335 repo_name = repo.repo_name
335 repo_name = repo.repo_name
336 if repo_group:
336 if repo_group:
337 repo_group_name = repo_group.group_name
337 repo_group_name = repo_group.group_name
338
338
339 org_query = query
339 org_query = query
340 if not query or len(query) < 3 or not searcher:
340 if not query or len(query) < 3 or not searcher:
341 return [], False
341 return [], False
342
342
343 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
343 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
344 if len(paths_re) != 1:
344 if len(paths_re) != 1:
345 return [], False
345 return [], False
346
346
347 file_path = paths_re[0]
347 file_path = paths_re[0]
348
348
349 search_path = searcher.escape_specials(file_path)
349 search_path = searcher.escape_specials(file_path)
350 result = searcher.search(
350 result = searcher.search(
351 'file.raw:*{}*'.format(search_path), 'path', auth_user,
351 'file.raw:*{}*'.format(search_path), 'path', auth_user,
352 repo_name, repo_group_name, raise_on_exc=False)
352 repo_name, repo_group_name, raise_on_exc=False)
353
353
354 files = []
354 files = []
355 for entry in result['results']:
355 for entry in result['results']:
356 repo_data = {
356 repo_data = {
357 'repository_id': entry.get('repository_id'),
357 'repository_id': entry.get('repository_id'),
358 'repository_type': entry.get('repo_type'),
358 'repository_type': entry.get('repo_type'),
359 'repository_name': entry.get('repository'),
359 'repository_name': entry.get('repository'),
360 }
360 }
361
361
362 file_entry = {
362 file_entry = {
363 'id': entry['commit_id'],
363 'id': entry['commit_id'],
364 'value': org_query,
364 'value': org_query,
365 'value_display': '`{}` file: {}'.format(
365 'value_display': '`{}` file: {}'.format(
366 entry['repository'], entry['file']),
366 entry['repository'], entry['file']),
367 'type': 'file',
367 'type': 'file',
368 'repo': entry['repository'],
368 'repo': entry['repository'],
369 'repo_data': repo_data,
369 'repo_data': repo_data,
370
370
371 'url': h.route_path(
371 'url': h.route_path(
372 'repo_files',
372 'repo_files',
373 repo_name=entry['repository'], commit_id=entry['commit_id'],
373 repo_name=entry['repository'], commit_id=entry['commit_id'],
374 f_path=entry['file'])
374 f_path=entry['file'])
375 }
375 }
376
376
377 files.append(file_entry)
377 files.append(file_entry)
378 return files, True
378 return files, True
379
379
380 @LoginRequired()
380 @LoginRequired()
381 @view_config(
381 @view_config(
382 route_name='repo_list_data', request_method='GET',
382 route_name='repo_list_data', request_method='GET',
383 renderer='json_ext', xhr=True)
383 renderer='json_ext', xhr=True)
384 def repo_list_data(self):
384 def repo_list_data(self):
385 _ = self.request.translate
385 _ = self.request.translate
386 self.load_default_context()
386 self.load_default_context()
387
387
388 query = self.request.GET.get('query')
388 query = self.request.GET.get('query')
389 repo_type = self.request.GET.get('repo_type')
389 repo_type = self.request.GET.get('repo_type')
390 log.debug('generating repo list, query:%s, repo_type:%s',
390 log.debug('generating repo list, query:%s, repo_type:%s',
391 query, repo_type)
391 query, repo_type)
392
392
393 res = []
393 res = []
394 repos = self._get_repo_list(query, repo_type=repo_type)
394 repos = self._get_repo_list(query, repo_type=repo_type)
395 if repos:
395 if repos:
396 res.append({
396 res.append({
397 'text': _('Repositories'),
397 'text': _('Repositories'),
398 'children': repos
398 'children': repos
399 })
399 })
400
400
401 data = {
401 data = {
402 'more': False,
402 'more': False,
403 'results': res
403 'results': res
404 }
404 }
405 return data
405 return data
406
406
407 @LoginRequired()
407 @LoginRequired()
408 @view_config(
408 @view_config(
409 route_name='repo_group_list_data', request_method='GET',
409 route_name='repo_group_list_data', request_method='GET',
410 renderer='json_ext', xhr=True)
410 renderer='json_ext', xhr=True)
411 def repo_group_list_data(self):
411 def repo_group_list_data(self):
412 _ = self.request.translate
412 _ = self.request.translate
413 self.load_default_context()
413 self.load_default_context()
414
414
415 query = self.request.GET.get('query')
415 query = self.request.GET.get('query')
416
416
417 log.debug('generating repo group list, query:%s',
417 log.debug('generating repo group list, query:%s',
418 query)
418 query)
419
419
420 res = []
420 res = []
421 repo_groups = self._get_repo_group_list(query)
421 repo_groups = self._get_repo_group_list(query)
422 if repo_groups:
422 if repo_groups:
423 res.append({
423 res.append({
424 'text': _('Repository Groups'),
424 'text': _('Repository Groups'),
425 'children': repo_groups
425 'children': repo_groups
426 })
426 })
427
427
428 data = {
428 data = {
429 'more': False,
429 'more': False,
430 'results': res
430 'results': res
431 }
431 }
432 return data
432 return data
433
433
434 def _get_default_search_queries(self, search_context, searcher, query):
434 def _get_default_search_queries(self, search_context, searcher, query):
435 if not searcher:
435 if not searcher:
436 return []
436 return []
437
437
438 is_es_6 = searcher.is_es_6
438 is_es_6 = searcher.is_es_6
439
439
440 queries = []
440 queries = []
441 repo_group_name, repo_name, repo_context = None, None, None
441 repo_group_name, repo_name, repo_context = None, None, None
442
442
443 # repo group context
443 # repo group context
444 if search_context.get('search_context[repo_group_name]'):
444 if search_context.get('search_context[repo_group_name]'):
445 repo_group_name = search_context.get('search_context[repo_group_name]')
445 repo_group_name = search_context.get('search_context[repo_group_name]')
446 if search_context.get('search_context[repo_name]'):
446 if search_context.get('search_context[repo_name]'):
447 repo_name = search_context.get('search_context[repo_name]')
447 repo_name = search_context.get('search_context[repo_name]')
448 repo_context = search_context.get('search_context[repo_view_type]')
448 repo_context = search_context.get('search_context[repo_view_type]')
449
449
450 if is_es_6 and repo_name:
450 if is_es_6 and repo_name:
451 # files
451 # files
452 def query_modifier():
452 def query_modifier():
453 qry = query
453 qry = query
454 return {'q': qry, 'type': 'content'}
454 return {'q': qry, 'type': 'content'}
455
455
456 label = u'File search for `{}`'.format(h.escape(query))
456 label = u'File content search for `{}`'.format(h.escape(query))
457 file_qry = {
457 file_qry = {
458 'id': -10,
458 'id': -10,
459 'value': query,
459 'value': query,
460 'value_display': label,
460 'value_display': label,
461 'value_icon': '<i class="icon-code"></i>',
461 'value_icon': '<i class="icon-code"></i>',
462 'type': 'search',
462 'type': 'search',
463 'subtype': 'repo',
463 'subtype': 'repo',
464 'url': h.route_path('search_repo',
464 'url': h.route_path('search_repo',
465 repo_name=repo_name,
465 repo_name=repo_name,
466 _query=query_modifier())
466 _query=query_modifier())
467 }
467 }
468
468
469 # commits
469 # commits
470 def query_modifier():
470 def query_modifier():
471 qry = query
471 qry = query
472 return {'q': qry, 'type': 'commit'}
472 return {'q': qry, 'type': 'commit'}
473
473
474 label = u'Commit search for `{}`'.format(h.escape(query))
474 label = u'Commit search for `{}`'.format(h.escape(query))
475 commit_qry = {
475 commit_qry = {
476 'id': -20,
476 'id': -20,
477 'value': query,
477 'value': query,
478 'value_display': label,
478 'value_display': label,
479 'value_icon': '<i class="icon-history"></i>',
479 'value_icon': '<i class="icon-history"></i>',
480 'type': 'search',
480 'type': 'search',
481 'subtype': 'repo',
481 'subtype': 'repo',
482 'url': h.route_path('search_repo',
482 'url': h.route_path('search_repo',
483 repo_name=repo_name,
483 repo_name=repo_name,
484 _query=query_modifier())
484 _query=query_modifier())
485 }
485 }
486
486
487 if repo_context in ['commit', 'commits']:
487 if repo_context in ['commit', 'commits']:
488 queries.extend([commit_qry, file_qry])
488 queries.extend([commit_qry, file_qry])
489 elif repo_context in ['files', 'summary']:
489 elif repo_context in ['files', 'summary']:
490 queries.extend([file_qry, commit_qry])
490 queries.extend([file_qry, commit_qry])
491 else:
491 else:
492 queries.extend([commit_qry, file_qry])
492 queries.extend([commit_qry, file_qry])
493
493
494 elif is_es_6 and repo_group_name:
494 elif is_es_6 and repo_group_name:
495 # files
495 # files
496 def query_modifier():
496 def query_modifier():
497 qry = query
497 qry = query
498 return {'q': qry, 'type': 'content'}
498 return {'q': qry, 'type': 'content'}
499
499
500 label = u'File search for `{}`'.format(query)
500 label = u'File content search for `{}`'.format(query)
501 file_qry = {
501 file_qry = {
502 'id': -30,
502 'id': -30,
503 'value': query,
503 'value': query,
504 'value_display': label,
504 'value_display': label,
505 'value_icon': '<i class="icon-code"></i>',
505 'value_icon': '<i class="icon-code"></i>',
506 'type': 'search',
506 'type': 'search',
507 'subtype': 'repo_group',
507 'subtype': 'repo_group',
508 'url': h.route_path('search_repo_group',
508 'url': h.route_path('search_repo_group',
509 repo_group_name=repo_group_name,
509 repo_group_name=repo_group_name,
510 _query=query_modifier())
510 _query=query_modifier())
511 }
511 }
512
512
513 # commits
513 # commits
514 def query_modifier():
514 def query_modifier():
515 qry = query
515 qry = query
516 return {'q': qry, 'type': 'commit'}
516 return {'q': qry, 'type': 'commit'}
517
517
518 label = u'Commit search for `{}`'.format(query)
518 label = u'Commit search for `{}`'.format(query)
519 commit_qry = {
519 commit_qry = {
520 'id': -40,
520 'id': -40,
521 'value': query,
521 'value': query,
522 'value_display': label,
522 'value_display': label,
523 'value_icon': '<i class="icon-history"></i>',
523 'value_icon': '<i class="icon-history"></i>',
524 'type': 'search',
524 'type': 'search',
525 'subtype': 'repo_group',
525 'subtype': 'repo_group',
526 'url': h.route_path('search_repo_group',
526 'url': h.route_path('search_repo_group',
527 repo_group_name=repo_group_name,
527 repo_group_name=repo_group_name,
528 _query=query_modifier())
528 _query=query_modifier())
529 }
529 }
530
530
531 if repo_context in ['commit', 'commits']:
531 if repo_context in ['commit', 'commits']:
532 queries.extend([commit_qry, file_qry])
532 queries.extend([commit_qry, file_qry])
533 elif repo_context in ['files', 'summary']:
533 elif repo_context in ['files', 'summary']:
534 queries.extend([file_qry, commit_qry])
534 queries.extend([file_qry, commit_qry])
535 else:
535 else:
536 queries.extend([commit_qry, file_qry])
536 queries.extend([commit_qry, file_qry])
537
537
538 # Global, not scoped
538 # Global, not scoped
539 if not queries:
539 if not queries:
540 queries.append(
540 queries.append(
541 {
541 {
542 'id': -1,
542 'id': -1,
543 'value': query,
543 'value': query,
544 'value_display': u'File search for: `{}`'.format(query),
544 'value_display': u'File content search for: `{}`'.format(query),
545 'value_icon': '<i class="icon-code"></i>',
545 'value_icon': '<i class="icon-code"></i>',
546 'type': 'search',
546 'type': 'search',
547 'subtype': 'global',
547 'subtype': 'global',
548 'url': h.route_path('search',
548 'url': h.route_path('search',
549 _query={'q': query, 'type': 'content'})
549 _query={'q': query, 'type': 'content'})
550 })
550 })
551 queries.append(
551 queries.append(
552 {
552 {
553 'id': -2,
553 'id': -2,
554 'value': query,
554 'value': query,
555 'value_display': u'Commit search for: `{}`'.format(query),
555 'value_display': u'Commit search for: `{}`'.format(query),
556 'value_icon': '<i class="icon-history"></i>',
556 'value_icon': '<i class="icon-history"></i>',
557 'type': 'search',
557 'type': 'search',
558 'subtype': 'global',
558 'subtype': 'global',
559 'url': h.route_path('search',
559 'url': h.route_path('search',
560 _query={'q': query, 'type': 'commit'})
560 _query={'q': query, 'type': 'commit'})
561 })
561 })
562
562
563 return queries
563 return queries
564
564
565 @LoginRequired()
565 @LoginRequired()
566 @view_config(
566 @view_config(
567 route_name='goto_switcher_data', request_method='GET',
567 route_name='goto_switcher_data', request_method='GET',
568 renderer='json_ext', xhr=True)
568 renderer='json_ext', xhr=True)
569 def goto_switcher_data(self):
569 def goto_switcher_data(self):
570 c = self.load_default_context()
570 c = self.load_default_context()
571
571
572 _ = self.request.translate
572 _ = self.request.translate
573
573
574 query = self.request.GET.get('query')
574 query = self.request.GET.get('query')
575 log.debug('generating main filter data, query %s', query)
575 log.debug('generating main filter data, query %s', query)
576
576
577 res = []
577 res = []
578 if not query:
578 if not query:
579 return {'suggestions': res}
579 return {'suggestions': res}
580
580
581 def no_match(name):
581 def no_match(name):
582 return {
582 return {
583 'id': -1,
583 'id': -1,
584 'value': "",
584 'value': "",
585 'value_display': name,
585 'value_display': name,
586 'type': 'text',
586 'type': 'text',
587 'url': ""
587 'url': ""
588 }
588 }
589 searcher = searcher_from_config(self.request.registry.settings)
589 searcher = searcher_from_config(self.request.registry.settings)
590 has_specialized_search = False
590 has_specialized_search = False
591
591
592 # set repo context
592 # set repo context
593 repo = None
593 repo = None
594 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
594 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
595 if repo_id:
595 if repo_id:
596 repo = Repository.get(repo_id)
596 repo = Repository.get(repo_id)
597
597
598 # set group context
598 # set group context
599 repo_group = None
599 repo_group = None
600 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
600 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
601 if repo_group_id:
601 if repo_group_id:
602 repo_group = RepoGroup.get(repo_group_id)
602 repo_group = RepoGroup.get(repo_group_id)
603 prefix_match = False
603 prefix_match = False
604
604
605 # user: type search
605 # user: type search
606 if not prefix_match:
606 if not prefix_match:
607 users, prefix_match = self._get_user_list(query)
607 users, prefix_match = self._get_user_list(query)
608 if users:
608 if users:
609 has_specialized_search = True
609 has_specialized_search = True
610 for serialized_user in users:
610 for serialized_user in users:
611 res.append(serialized_user)
611 res.append(serialized_user)
612 elif prefix_match:
612 elif prefix_match:
613 has_specialized_search = True
613 has_specialized_search = True
614 res.append(no_match('No matching users found'))
614 res.append(no_match('No matching users found'))
615
615
616 # user_group: type search
616 # user_group: type search
617 if not prefix_match:
617 if not prefix_match:
618 user_groups, prefix_match = self._get_user_groups_list(query)
618 user_groups, prefix_match = self._get_user_groups_list(query)
619 if user_groups:
619 if user_groups:
620 has_specialized_search = True
620 has_specialized_search = True
621 for serialized_user_group in user_groups:
621 for serialized_user_group in user_groups:
622 res.append(serialized_user_group)
622 res.append(serialized_user_group)
623 elif prefix_match:
623 elif prefix_match:
624 has_specialized_search = True
624 has_specialized_search = True
625 res.append(no_match('No matching user groups found'))
625 res.append(no_match('No matching user groups found'))
626
626
627 # FTS commit: type search
627 # FTS commit: type search
628 if not prefix_match:
628 if not prefix_match:
629 commits, prefix_match = self._get_hash_commit_list(
629 commits, prefix_match = self._get_hash_commit_list(
630 c.auth_user, searcher, query, repo, repo_group)
630 c.auth_user, searcher, query, repo, repo_group)
631 if commits:
631 if commits:
632 has_specialized_search = True
632 has_specialized_search = True
633 unique_repos = collections.OrderedDict()
633 unique_repos = collections.OrderedDict()
634 for commit in commits:
634 for commit in commits:
635 repo_name = commit['repo']
635 repo_name = commit['repo']
636 unique_repos.setdefault(repo_name, []).append(commit)
636 unique_repos.setdefault(repo_name, []).append(commit)
637
637
638 for _repo, commits in unique_repos.items():
638 for _repo, commits in unique_repos.items():
639 for commit in commits:
639 for commit in commits:
640 res.append(commit)
640 res.append(commit)
641 elif prefix_match:
641 elif prefix_match:
642 has_specialized_search = True
642 has_specialized_search = True
643 res.append(no_match('No matching commits found'))
643 res.append(no_match('No matching commits found'))
644
644
645 # FTS file: type search
645 # FTS file: type search
646 if not prefix_match:
646 if not prefix_match:
647 paths, prefix_match = self._get_path_list(
647 paths, prefix_match = self._get_path_list(
648 c.auth_user, searcher, query, repo, repo_group)
648 c.auth_user, searcher, query, repo, repo_group)
649 if paths:
649 if paths:
650 has_specialized_search = True
650 has_specialized_search = True
651 unique_repos = collections.OrderedDict()
651 unique_repos = collections.OrderedDict()
652 for path in paths:
652 for path in paths:
653 repo_name = path['repo']
653 repo_name = path['repo']
654 unique_repos.setdefault(repo_name, []).append(path)
654 unique_repos.setdefault(repo_name, []).append(path)
655
655
656 for repo, paths in unique_repos.items():
656 for repo, paths in unique_repos.items():
657 for path in paths:
657 for path in paths:
658 res.append(path)
658 res.append(path)
659 elif prefix_match:
659 elif prefix_match:
660 has_specialized_search = True
660 has_specialized_search = True
661 res.append(no_match('No matching files found'))
661 res.append(no_match('No matching files found'))
662
662
663 # main suggestions
663 # main suggestions
664 if not has_specialized_search:
664 if not has_specialized_search:
665 repo_group_name = ''
665 repo_group_name = ''
666 if repo_group:
666 if repo_group:
667 repo_group_name = repo_group.group_name
667 repo_group_name = repo_group.group_name
668
668
669 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
669 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
670 res.append(_q)
670 res.append(_q)
671
671
672 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
672 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
673 for serialized_repo_group in repo_groups:
673 for serialized_repo_group in repo_groups:
674 res.append(serialized_repo_group)
674 res.append(serialized_repo_group)
675
675
676 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
676 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
677 for serialized_repo in repos:
677 for serialized_repo in repos:
678 res.append(serialized_repo)
678 res.append(serialized_repo)
679
679
680 if not repos and not repo_groups:
680 if not repos and not repo_groups:
681 res.append(no_match('No matches found'))
681 res.append(no_match('No matches found'))
682
682
683 return {'suggestions': res}
683 return {'suggestions': res}
684
684
685 @LoginRequired()
685 @LoginRequired()
686 @view_config(
686 @view_config(
687 route_name='home', request_method='GET',
687 route_name='home', request_method='GET',
688 renderer='rhodecode:templates/index.mako')
688 renderer='rhodecode:templates/index.mako')
689 def main_page(self):
689 def main_page(self):
690 c = self.load_default_context()
690 c = self.load_default_context()
691 c.repo_group = None
691 c.repo_group = None
692 return self._get_template_context(c)
692 return self._get_template_context(c)
693
693
694 def _main_page_repo_groups_data(self, repo_group_id):
694 def _main_page_repo_groups_data(self, repo_group_id):
695 column_map = {
695 column_map = {
696 'name': 'group_name_hash',
696 'name': 'group_name_hash',
697 'desc': 'group_description',
697 'desc': 'group_description',
698 'last_change': 'updated_on',
698 'last_change': 'updated_on',
699 'owner': 'user_username',
699 'owner': 'user_username',
700 }
700 }
701 draw, start, limit = self._extract_chunk(self.request)
701 draw, start, limit = self._extract_chunk(self.request)
702 search_q, order_by, order_dir = self._extract_ordering(
702 search_q, order_by, order_dir = self._extract_ordering(
703 self.request, column_map=column_map)
703 self.request, column_map=column_map)
704 return RepoGroupModel().get_repo_groups_data_table(
704 return RepoGroupModel().get_repo_groups_data_table(
705 draw, start, limit,
705 draw, start, limit,
706 search_q, order_by, order_dir,
706 search_q, order_by, order_dir,
707 self._rhodecode_user, repo_group_id)
707 self._rhodecode_user, repo_group_id)
708
708
709 def _main_page_repos_data(self, repo_group_id):
709 def _main_page_repos_data(self, repo_group_id):
710 column_map = {
710 column_map = {
711 'name': 'repo_name',
711 'name': 'repo_name',
712 'desc': 'description',
712 'desc': 'description',
713 'last_change': 'updated_on',
713 'last_change': 'updated_on',
714 'owner': 'user_username',
714 'owner': 'user_username',
715 }
715 }
716 draw, start, limit = self._extract_chunk(self.request)
716 draw, start, limit = self._extract_chunk(self.request)
717 search_q, order_by, order_dir = self._extract_ordering(
717 search_q, order_by, order_dir = self._extract_ordering(
718 self.request, column_map=column_map)
718 self.request, column_map=column_map)
719 return RepoModel().get_repos_data_table(
719 return RepoModel().get_repos_data_table(
720 draw, start, limit,
720 draw, start, limit,
721 search_q, order_by, order_dir,
721 search_q, order_by, order_dir,
722 self._rhodecode_user, repo_group_id)
722 self._rhodecode_user, repo_group_id)
723
723
724 @LoginRequired()
724 @LoginRequired()
725 @view_config(
725 @view_config(
726 route_name='main_page_repo_groups_data',
726 route_name='main_page_repo_groups_data',
727 request_method='GET', renderer='json_ext', xhr=True)
727 request_method='GET', renderer='json_ext', xhr=True)
728 def main_page_repo_groups_data(self):
728 def main_page_repo_groups_data(self):
729 self.load_default_context()
729 self.load_default_context()
730 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
730 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
731
731
732 if repo_group_id:
732 if repo_group_id:
733 group = RepoGroup.get_or_404(repo_group_id)
733 group = RepoGroup.get_or_404(repo_group_id)
734 _perms = AuthUser.repo_group_read_perms
734 _perms = AuthUser.repo_group_read_perms
735 if not HasRepoGroupPermissionAny(*_perms)(
735 if not HasRepoGroupPermissionAny(*_perms)(
736 group.group_name, 'user is allowed to list repo group children'):
736 group.group_name, 'user is allowed to list repo group children'):
737 raise HTTPNotFound()
737 raise HTTPNotFound()
738
738
739 return self._main_page_repo_groups_data(repo_group_id)
739 return self._main_page_repo_groups_data(repo_group_id)
740
740
741 @LoginRequired()
741 @LoginRequired()
742 @view_config(
742 @view_config(
743 route_name='main_page_repos_data',
743 route_name='main_page_repos_data',
744 request_method='GET', renderer='json_ext', xhr=True)
744 request_method='GET', renderer='json_ext', xhr=True)
745 def main_page_repos_data(self):
745 def main_page_repos_data(self):
746 self.load_default_context()
746 self.load_default_context()
747 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
747 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
748
748
749 if repo_group_id:
749 if repo_group_id:
750 group = RepoGroup.get_or_404(repo_group_id)
750 group = RepoGroup.get_or_404(repo_group_id)
751 _perms = AuthUser.repo_group_read_perms
751 _perms = AuthUser.repo_group_read_perms
752 if not HasRepoGroupPermissionAny(*_perms)(
752 if not HasRepoGroupPermissionAny(*_perms)(
753 group.group_name, 'user is allowed to list repo group children'):
753 group.group_name, 'user is allowed to list repo group children'):
754 raise HTTPNotFound()
754 raise HTTPNotFound()
755
755
756 return self._main_page_repos_data(repo_group_id)
756 return self._main_page_repos_data(repo_group_id)
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
759 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
760 @view_config(
760 @view_config(
761 route_name='repo_group_home', request_method='GET',
761 route_name='repo_group_home', request_method='GET',
762 renderer='rhodecode:templates/index_repo_group.mako')
762 renderer='rhodecode:templates/index_repo_group.mako')
763 @view_config(
763 @view_config(
764 route_name='repo_group_home_slash', request_method='GET',
764 route_name='repo_group_home_slash', request_method='GET',
765 renderer='rhodecode:templates/index_repo_group.mako')
765 renderer='rhodecode:templates/index_repo_group.mako')
766 def repo_group_main_page(self):
766 def repo_group_main_page(self):
767 c = self.load_default_context()
767 c = self.load_default_context()
768 c.repo_group = self.request.db_repo_group
768 c.repo_group = self.request.db_repo_group
769 return self._get_template_context(c)
769 return self._get_template_context(c)
770
770
771 @LoginRequired()
771 @LoginRequired()
772 @CSRFRequired()
772 @CSRFRequired()
773 @view_config(
773 @view_config(
774 route_name='markup_preview', request_method='POST',
774 route_name='markup_preview', request_method='POST',
775 renderer='string', xhr=True)
775 renderer='string', xhr=True)
776 def markup_preview(self):
776 def markup_preview(self):
777 # Technically a CSRF token is not needed as no state changes with this
777 # Technically a CSRF token is not needed as no state changes with this
778 # call. However, as this is a POST is better to have it, so automated
778 # call. However, as this is a POST is better to have it, so automated
779 # tools don't flag it as potential CSRF.
779 # tools don't flag it as potential CSRF.
780 # Post is required because the payload could be bigger than the maximum
780 # Post is required because the payload could be bigger than the maximum
781 # allowed by GET.
781 # allowed by GET.
782
782
783 text = self.request.POST.get('text')
783 text = self.request.POST.get('text')
784 renderer = self.request.POST.get('renderer') or 'rst'
784 renderer = self.request.POST.get('renderer') or 'rst'
785 if text:
785 if text:
786 return h.render(text, renderer=renderer, mentions=True)
786 return h.render(text, renderer=renderer, mentions=True)
787 return ''
787 return ''
788
788
789 @LoginRequired()
789 @LoginRequired()
790 @CSRFRequired()
790 @CSRFRequired()
791 @view_config(
791 @view_config(
792 route_name='file_preview', request_method='POST',
792 route_name='file_preview', request_method='POST',
793 renderer='string', xhr=True)
793 renderer='string', xhr=True)
794 def file_preview(self):
794 def file_preview(self):
795 # Technically a CSRF token is not needed as no state changes with this
795 # Technically a CSRF token is not needed as no state changes with this
796 # call. However, as this is a POST is better to have it, so automated
796 # call. However, as this is a POST is better to have it, so automated
797 # tools don't flag it as potential CSRF.
797 # tools don't flag it as potential CSRF.
798 # Post is required because the payload could be bigger than the maximum
798 # Post is required because the payload could be bigger than the maximum
799 # allowed by GET.
799 # allowed by GET.
800
800
801 text = self.request.POST.get('text')
801 text = self.request.POST.get('text')
802 file_path = self.request.POST.get('file_path')
802 file_path = self.request.POST.get('file_path')
803
803
804 renderer = h.renderer_from_filename(file_path)
804 renderer = h.renderer_from_filename(file_path)
805
805
806 if renderer:
806 if renderer:
807 return h.render(text, renderer=renderer, mentions=True)
807 return h.render(text, renderer=renderer, mentions=True)
808 else:
808 else:
809 self.load_default_context()
809 self.load_default_context()
810 _render = self.request.get_partial_renderer(
810 _render = self.request.get_partial_renderer(
811 'rhodecode:templates/files/file_content.mako')
811 'rhodecode:templates/files/file_content.mako')
812
812
813 lines = filenode_as_lines_tokens(FileNode(file_path, text))
813 lines = filenode_as_lines_tokens(FileNode(file_path, text))
814
814
815 return _render('render_lines', lines)
815 return _render('render_lines', lines)
816
816
817 @LoginRequired()
817 @LoginRequired()
818 @CSRFRequired()
818 @CSRFRequired()
819 @view_config(
819 @view_config(
820 route_name='store_user_session_value', request_method='POST',
820 route_name='store_user_session_value', request_method='POST',
821 renderer='string', xhr=True)
821 renderer='string', xhr=True)
822 def store_user_session_attr(self):
822 def store_user_session_attr(self):
823 key = self.request.POST.get('key')
823 key = self.request.POST.get('key')
824 val = self.request.POST.get('val')
824 val = self.request.POST.get('val')
825
825
826 existing_value = self.request.session.get(key)
826 existing_value = self.request.session.get(key)
827 if existing_value != val:
827 if existing_value != val:
828 self.request.session[key] = val
828 self.request.session[key] = val
829
829
830 return 'stored:{}:{}'.format(key, val)
830 return 'stored:{}:{}'.format(key, val)
@@ -1,41 +1,45 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2018-2019 RhodeCode GmbH
3 # Copyright (C) 2018-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 def includeme(config):
22 def includeme(config):
23
23
24 config.add_route(
24 config.add_route(
25 name='hovercard_user',
25 name='hovercard_user',
26 pattern='/_hovercard/user/{user_id}')
26 pattern='/_hovercard/user/{user_id}')
27
27
28 config.add_route(
28 config.add_route(
29 name='hovercard_username',
30 pattern='/_hovercard/username/{username}')
31
32 config.add_route(
29 name='hovercard_user_group',
33 name='hovercard_user_group',
30 pattern='/_hovercard/user_group/{user_group_id}')
34 pattern='/_hovercard/user_group/{user_group_id}')
31
35
32 config.add_route(
36 config.add_route(
33 name='hovercard_pull_request',
37 name='hovercard_pull_request',
34 pattern='/_hovercard/pull_request/{pull_request_id}')
38 pattern='/_hovercard/pull_request/{pull_request_id}')
35
39
36 config.add_route(
40 config.add_route(
37 name='hovercard_repo_commit',
41 name='hovercard_repo_commit',
38 pattern='/_hovercard/commit/{repo_name:.*?[^/]}/{commit_id}', repo_route=True)
42 pattern='/_hovercard/commit/{repo_name:.*?[^/]}/{commit_id}', repo_route=True)
39
43
40 # Scan module for configuration decorators.
44 # Scan module for configuration decorators.
41 config.scan('.views', ignore='.tests')
45 config.scan('.views', ignore='.tests')
@@ -1,110 +1,123 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound
25 from pyramid.httpexceptions import HTTPNotFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27
27
28 from rhodecode.apps._base import BaseAppView, RepoAppView
28 from rhodecode.apps._base import BaseAppView, RepoAppView
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 HasRepoPermissionAnyDecorator)
32 HasRepoPermissionAnyDecorator)
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 from rhodecode.lib.index import searcher_from_config
34 from rhodecode.lib.index import searcher_from_config
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
41 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo_group import RepoGroupModel
42 from rhodecode.model.repo_group import RepoGroupModel
43 from rhodecode.model.scm import RepoGroupList, RepoList
43 from rhodecode.model.scm import RepoGroupList, RepoList
44 from rhodecode.model.user import UserModel
44 from rhodecode.model.user import UserModel
45 from rhodecode.model.user_group import UserGroupModel
45 from rhodecode.model.user_group import UserGroupModel
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class HoverCardsView(BaseAppView):
50 class HoverCardsView(BaseAppView):
51
51
52 def load_default_context(self):
52 def load_default_context(self):
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54 return c
54 return c
55
55
56 @LoginRequired()
56 @LoginRequired()
57 @view_config(
57 @view_config(
58 route_name='hovercard_user', request_method='GET', xhr=True,
58 route_name='hovercard_user', request_method='GET', xhr=True,
59 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
59 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
60 def hovercard_user(self):
60 def hovercard_user(self):
61 c = self.load_default_context()
61 c = self.load_default_context()
62 user_id = self.request.matchdict['user_id']
62 user_id = self.request.matchdict['user_id']
63 c.user = User.get_or_404(user_id)
63 c.user = User.get_or_404(user_id)
64 return self._get_template_context(c)
64 return self._get_template_context(c)
65
65
66 @LoginRequired()
66 @LoginRequired()
67 @view_config(
67 @view_config(
68 route_name='hovercard_username', request_method='GET', xhr=True,
69 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
70 def hovercard_username(self):
71 c = self.load_default_context()
72 username = self.request.matchdict['username']
73 c.user = User.get_by_username(username)
74 if not c.user:
75 raise HTTPNotFound()
76
77 return self._get_template_context(c)
78
79 @LoginRequired()
80 @view_config(
68 route_name='hovercard_user_group', request_method='GET', xhr=True,
81 route_name='hovercard_user_group', request_method='GET', xhr=True,
69 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
82 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
70 def hovercard_user_group(self):
83 def hovercard_user_group(self):
71 c = self.load_default_context()
84 c = self.load_default_context()
72 user_group_id = self.request.matchdict['user_group_id']
85 user_group_id = self.request.matchdict['user_group_id']
73 c.user_group = UserGroup.get_or_404(user_group_id)
86 c.user_group = UserGroup.get_or_404(user_group_id)
74 return self._get_template_context(c)
87 return self._get_template_context(c)
75
88
76 @LoginRequired()
89 @LoginRequired()
77 @view_config(
90 @view_config(
78 route_name='hovercard_pull_request', request_method='GET', xhr=True,
91 route_name='hovercard_pull_request', request_method='GET', xhr=True,
79 renderer='rhodecode:templates/hovercards/hovercard_pull_request.mako')
92 renderer='rhodecode:templates/hovercards/hovercard_pull_request.mako')
80 def hovercard_pull_request(self):
93 def hovercard_pull_request(self):
81 c = self.load_default_context()
94 c = self.load_default_context()
82 c.pull_request = PullRequest.get_or_404(
95 c.pull_request = PullRequest.get_or_404(
83 self.request.matchdict['pull_request_id'])
96 self.request.matchdict['pull_request_id'])
84 perms = ['repository.read', 'repository.write', 'repository.admin']
97 perms = ['repository.read', 'repository.write', 'repository.admin']
85 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
98 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
86 c.pull_request.target_repo.repo_name)
99 c.pull_request.target_repo.repo_name)
87 return self._get_template_context(c)
100 return self._get_template_context(c)
88
101
89
102
90 class HoverCardsRepoView(RepoAppView):
103 class HoverCardsRepoView(RepoAppView):
91 def load_default_context(self):
104 def load_default_context(self):
92 c = self._get_local_tmpl_context()
105 c = self._get_local_tmpl_context()
93 return c
106 return c
94
107
95 @LoginRequired()
108 @LoginRequired()
96 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
109 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
97 @view_config(
110 @view_config(
98 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
111 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
99 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
112 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
100 def hovercard_repo_commit(self):
113 def hovercard_repo_commit(self):
101 c = self.load_default_context()
114 c = self.load_default_context()
102 commit_id = self.request.matchdict['commit_id']
115 commit_id = self.request.matchdict['commit_id']
103 pre_load = ['author', 'branch', 'date', 'message']
116 pre_load = ['author', 'branch', 'date', 'message']
104 try:
117 try:
105 c.commit = self.rhodecode_vcs_repo.get_commit(
118 c.commit = self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id, pre_load=pre_load)
119 commit_id=commit_id, pre_load=pre_load)
107 except (CommitDoesNotExistError, EmptyRepositoryError):
120 except (CommitDoesNotExistError, EmptyRepositoryError):
108 raise HTTPNotFound()
121 raise HTTPNotFound()
109
122
110 return self._get_template_context(c)
123 return self._get_template_context(c)
@@ -1,578 +1,580 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 no_newline_id_generator)
28 no_newline_id_generator)
29 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib.auth import check_password
30 from rhodecode.lib.auth import check_password
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.db import User, Notification, UserApiKeys
33 from rhodecode.model.db import User, Notification, UserApiKeys
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35
35
36 fixture = Fixture()
36 fixture = Fixture()
37
37
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39
39
40
40
41 def route_path(name, params=None, **kwargs):
41 def route_path(name, params=None, **kwargs):
42 import urllib
42 import urllib
43 from rhodecode.apps._base import ADMIN_PREFIX
43 from rhodecode.apps._base import ADMIN_PREFIX
44
44
45 base_url = {
45 base_url = {
46 'login': ADMIN_PREFIX + '/login',
46 'login': ADMIN_PREFIX + '/login',
47 'logout': ADMIN_PREFIX + '/logout',
47 'logout': ADMIN_PREFIX + '/logout',
48 'register': ADMIN_PREFIX + '/register',
48 'register': ADMIN_PREFIX + '/register',
49 'reset_password':
49 'reset_password':
50 ADMIN_PREFIX + '/password_reset',
50 ADMIN_PREFIX + '/password_reset',
51 'reset_password_confirmation':
51 'reset_password_confirmation':
52 ADMIN_PREFIX + '/password_reset_confirmation',
52 ADMIN_PREFIX + '/password_reset_confirmation',
53
53
54 'admin_permissions_application':
54 'admin_permissions_application':
55 ADMIN_PREFIX + '/permissions/application',
55 ADMIN_PREFIX + '/permissions/application',
56 'admin_permissions_application_update':
56 'admin_permissions_application_update':
57 ADMIN_PREFIX + '/permissions/application/update',
57 ADMIN_PREFIX + '/permissions/application/update',
58
58
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60
60
61 }[name].format(**kwargs)
61 }[name].format(**kwargs)
62
62
63 if params:
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
65 return base_url
65 return base_url
66
66
67
67
68 @pytest.mark.usefixtures('app')
68 @pytest.mark.usefixtures('app')
69 class TestLoginController(object):
69 class TestLoginController(object):
70 destroy_users = set()
70 destroy_users = set()
71
71
72 @classmethod
72 @classmethod
73 def teardown_class(cls):
73 def teardown_class(cls):
74 fixture.destroy_users(cls.destroy_users)
74 fixture.destroy_users(cls.destroy_users)
75
75
76 def teardown_method(self, method):
76 def teardown_method(self, method):
77 for n in Notification.query().all():
77 for n in Notification.query().all():
78 Session().delete(n)
78 Session().delete(n)
79
79
80 Session().commit()
80 Session().commit()
81 assert Notification.query().all() == []
81 assert Notification.query().all() == []
82
82
83 def test_index(self):
83 def test_index(self):
84 response = self.app.get(route_path('login'))
84 response = self.app.get(route_path('login'))
85 assert response.status == '200 OK'
85 assert response.status == '200 OK'
86 # Test response...
86 # Test response...
87
87
88 def test_login_admin_ok(self):
88 def test_login_admin_ok(self):
89 response = self.app.post(route_path('login'),
89 response = self.app.post(route_path('login'),
90 {'username': 'test_admin',
90 {'username': 'test_admin',
91 'password': 'test12'}, status=302)
91 'password': 'test12'}, status=302)
92 response = response.follow()
92 response = response.follow()
93 session = response.get_session_from_response()
93 session = response.get_session_from_response()
94 username = session['rhodecode_user'].get('username')
94 username = session['rhodecode_user'].get('username')
95 assert username == 'test_admin'
95 assert username == 'test_admin'
96 response.mustcontain('logout')
96 response.mustcontain('logout')
97
97
98 def test_login_regular_ok(self):
98 def test_login_regular_ok(self):
99 response = self.app.post(route_path('login'),
99 response = self.app.post(route_path('login'),
100 {'username': 'test_regular',
100 {'username': 'test_regular',
101 'password': 'test12'}, status=302)
101 'password': 'test12'}, status=302)
102
102
103 response = response.follow()
103 response = response.follow()
104 session = response.get_session_from_response()
104 session = response.get_session_from_response()
105 username = session['rhodecode_user'].get('username')
105 username = session['rhodecode_user'].get('username')
106 assert username == 'test_regular'
106 assert username == 'test_regular'
107 response.mustcontain('logout')
107 response.mustcontain('logout')
108
108
109 def test_login_regular_forbidden_when_super_admin_restriction(self):
109 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 with fixture.auth_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
111 with fixture.auth_restriction(self.app._pyramid_registry,
112 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
112 response = self.app.post(route_path('login'),
113 response = self.app.post(route_path('login'),
113 {'username': 'test_regular',
114 {'username': 'test_regular',
114 'password': 'test12'})
115 'password': 'test12'})
115
116
116 response.mustcontain('invalid user name')
117 response.mustcontain('invalid user name')
117 response.mustcontain('invalid password')
118 response.mustcontain('invalid password')
118
119
119 def test_login_regular_forbidden_when_scope_restriction(self):
120 def test_login_regular_forbidden_when_scope_restriction(self):
120 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
121 with fixture.scope_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
122 with fixture.scope_restriction(self.app._pyramid_registry,
123 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
122 response = self.app.post(route_path('login'),
124 response = self.app.post(route_path('login'),
123 {'username': 'test_regular',
125 {'username': 'test_regular',
124 'password': 'test12'})
126 'password': 'test12'})
125
127
126 response.mustcontain('invalid user name')
128 response.mustcontain('invalid user name')
127 response.mustcontain('invalid password')
129 response.mustcontain('invalid password')
128
130
129 def test_login_ok_came_from(self):
131 def test_login_ok_came_from(self):
130 test_came_from = '/_admin/users?branch=stable'
132 test_came_from = '/_admin/users?branch=stable'
131 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
133 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
132 response = self.app.post(
134 response = self.app.post(
133 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
135 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
134
136
135 assert 'branch=stable' in response.location
137 assert 'branch=stable' in response.location
136 response = response.follow()
138 response = response.follow()
137
139
138 assert response.status == '200 OK'
140 assert response.status == '200 OK'
139 response.mustcontain('Users administration')
141 response.mustcontain('Users administration')
140
142
141 def test_redirect_to_login_with_get_args(self):
143 def test_redirect_to_login_with_get_args(self):
142 with fixture.anon_access(False):
144 with fixture.anon_access(False):
143 kwargs = {'branch': 'stable'}
145 kwargs = {'branch': 'stable'}
144 response = self.app.get(
146 response = self.app.get(
145 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
147 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
146 status=302)
148 status=302)
147
149
148 response_query = urlparse.parse_qsl(response.location)
150 response_query = urlparse.parse_qsl(response.location)
149 assert 'branch=stable' in response_query[0][1]
151 assert 'branch=stable' in response_query[0][1]
150
152
151 def test_login_form_with_get_args(self):
153 def test_login_form_with_get_args(self):
152 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
154 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
153 response = self.app.get(_url)
155 response = self.app.get(_url)
154 assert 'branch%3Dstable' in response.form.action
156 assert 'branch%3Dstable' in response.form.action
155
157
156 @pytest.mark.parametrize("url_came_from", [
158 @pytest.mark.parametrize("url_came_from", [
157 'data:text/html,<script>window.alert("xss")</script>',
159 'data:text/html,<script>window.alert("xss")</script>',
158 'mailto:test@rhodecode.org',
160 'mailto:test@rhodecode.org',
159 'file:///etc/passwd',
161 'file:///etc/passwd',
160 'ftp://some.ftp.server',
162 'ftp://some.ftp.server',
161 'http://other.domain',
163 'http://other.domain',
162 '/\r\nX-Forwarded-Host: http://example.org',
164 '/\r\nX-Forwarded-Host: http://example.org',
163 ], ids=no_newline_id_generator)
165 ], ids=no_newline_id_generator)
164 def test_login_bad_came_froms(self, url_came_from):
166 def test_login_bad_came_froms(self, url_came_from):
165 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
166 response = self.app.post(
168 response = self.app.post(
167 _url,
169 _url,
168 {'username': 'test_admin', 'password': 'test12'})
170 {'username': 'test_admin', 'password': 'test12'})
169 assert response.status == '302 Found'
171 assert response.status == '302 Found'
170 response = response.follow()
172 response = response.follow()
171 assert response.status == '200 OK'
173 assert response.status == '200 OK'
172 assert response.request.path == '/'
174 assert response.request.path == '/'
173
175
174 def test_login_short_password(self):
176 def test_login_short_password(self):
175 response = self.app.post(route_path('login'),
177 response = self.app.post(route_path('login'),
176 {'username': 'test_admin',
178 {'username': 'test_admin',
177 'password': 'as'})
179 'password': 'as'})
178 assert response.status == '200 OK'
180 assert response.status == '200 OK'
179
181
180 response.mustcontain('Enter 3 characters or more')
182 response.mustcontain('Enter 3 characters or more')
181
183
182 def test_login_wrong_non_ascii_password(self, user_regular):
184 def test_login_wrong_non_ascii_password(self, user_regular):
183 response = self.app.post(
185 response = self.app.post(
184 route_path('login'),
186 route_path('login'),
185 {'username': user_regular.username,
187 {'username': user_regular.username,
186 'password': u'invalid-non-asci\xe4'.encode('utf8')})
188 'password': u'invalid-non-asci\xe4'.encode('utf8')})
187
189
188 response.mustcontain('invalid user name')
190 response.mustcontain('invalid user name')
189 response.mustcontain('invalid password')
191 response.mustcontain('invalid password')
190
192
191 def test_login_with_non_ascii_password(self, user_util):
193 def test_login_with_non_ascii_password(self, user_util):
192 password = u'valid-non-ascii\xe4'
194 password = u'valid-non-ascii\xe4'
193 user = user_util.create_user(password=password)
195 user = user_util.create_user(password=password)
194 response = self.app.post(
196 response = self.app.post(
195 route_path('login'),
197 route_path('login'),
196 {'username': user.username,
198 {'username': user.username,
197 'password': password.encode('utf-8')})
199 'password': password.encode('utf-8')})
198 assert response.status_code == 302
200 assert response.status_code == 302
199
201
200 def test_login_wrong_username_password(self):
202 def test_login_wrong_username_password(self):
201 response = self.app.post(route_path('login'),
203 response = self.app.post(route_path('login'),
202 {'username': 'error',
204 {'username': 'error',
203 'password': 'test12'})
205 'password': 'test12'})
204
206
205 response.mustcontain('invalid user name')
207 response.mustcontain('invalid user name')
206 response.mustcontain('invalid password')
208 response.mustcontain('invalid password')
207
209
208 def test_login_admin_ok_password_migration(self, real_crypto_backend):
210 def test_login_admin_ok_password_migration(self, real_crypto_backend):
209 from rhodecode.lib import auth
211 from rhodecode.lib import auth
210
212
211 # create new user, with sha256 password
213 # create new user, with sha256 password
212 temp_user = 'test_admin_sha256'
214 temp_user = 'test_admin_sha256'
213 user = fixture.create_user(temp_user)
215 user = fixture.create_user(temp_user)
214 user.password = auth._RhodeCodeCryptoSha256().hash_create(
216 user.password = auth._RhodeCodeCryptoSha256().hash_create(
215 b'test123')
217 b'test123')
216 Session().add(user)
218 Session().add(user)
217 Session().commit()
219 Session().commit()
218 self.destroy_users.add(temp_user)
220 self.destroy_users.add(temp_user)
219 response = self.app.post(route_path('login'),
221 response = self.app.post(route_path('login'),
220 {'username': temp_user,
222 {'username': temp_user,
221 'password': 'test123'}, status=302)
223 'password': 'test123'}, status=302)
222
224
223 response = response.follow()
225 response = response.follow()
224 session = response.get_session_from_response()
226 session = response.get_session_from_response()
225 username = session['rhodecode_user'].get('username')
227 username = session['rhodecode_user'].get('username')
226 assert username == temp_user
228 assert username == temp_user
227 response.mustcontain('logout')
229 response.mustcontain('logout')
228
230
229 # new password should be bcrypted, after log-in and transfer
231 # new password should be bcrypted, after log-in and transfer
230 user = User.get_by_username(temp_user)
232 user = User.get_by_username(temp_user)
231 assert user.password.startswith('$')
233 assert user.password.startswith('$')
232
234
233 # REGISTRATIONS
235 # REGISTRATIONS
234 def test_register(self):
236 def test_register(self):
235 response = self.app.get(route_path('register'))
237 response = self.app.get(route_path('register'))
236 response.mustcontain('Create an Account')
238 response.mustcontain('Create an Account')
237
239
238 def test_register_err_same_username(self):
240 def test_register_err_same_username(self):
239 uname = 'test_admin'
241 uname = 'test_admin'
240 response = self.app.post(
242 response = self.app.post(
241 route_path('register'),
243 route_path('register'),
242 {
244 {
243 'username': uname,
245 'username': uname,
244 'password': 'test12',
246 'password': 'test12',
245 'password_confirmation': 'test12',
247 'password_confirmation': 'test12',
246 'email': 'goodmail@domain.com',
248 'email': 'goodmail@domain.com',
247 'firstname': 'test',
249 'firstname': 'test',
248 'lastname': 'test'
250 'lastname': 'test'
249 }
251 }
250 )
252 )
251
253
252 assertr = response.assert_response()
254 assertr = response.assert_response()
253 msg = 'Username "%(username)s" already exists'
255 msg = 'Username "%(username)s" already exists'
254 msg = msg % {'username': uname}
256 msg = msg % {'username': uname}
255 assertr.element_contains('#username+.error-message', msg)
257 assertr.element_contains('#username+.error-message', msg)
256
258
257 def test_register_err_same_email(self):
259 def test_register_err_same_email(self):
258 response = self.app.post(
260 response = self.app.post(
259 route_path('register'),
261 route_path('register'),
260 {
262 {
261 'username': 'test_admin_0',
263 'username': 'test_admin_0',
262 'password': 'test12',
264 'password': 'test12',
263 'password_confirmation': 'test12',
265 'password_confirmation': 'test12',
264 'email': 'test_admin@mail.com',
266 'email': 'test_admin@mail.com',
265 'firstname': 'test',
267 'firstname': 'test',
266 'lastname': 'test'
268 'lastname': 'test'
267 }
269 }
268 )
270 )
269
271
270 assertr = response.assert_response()
272 assertr = response.assert_response()
271 msg = u'This e-mail address is already taken'
273 msg = u'This e-mail address is already taken'
272 assertr.element_contains('#email+.error-message', msg)
274 assertr.element_contains('#email+.error-message', msg)
273
275
274 def test_register_err_same_email_case_sensitive(self):
276 def test_register_err_same_email_case_sensitive(self):
275 response = self.app.post(
277 response = self.app.post(
276 route_path('register'),
278 route_path('register'),
277 {
279 {
278 'username': 'test_admin_1',
280 'username': 'test_admin_1',
279 'password': 'test12',
281 'password': 'test12',
280 'password_confirmation': 'test12',
282 'password_confirmation': 'test12',
281 'email': 'TesT_Admin@mail.COM',
283 'email': 'TesT_Admin@mail.COM',
282 'firstname': 'test',
284 'firstname': 'test',
283 'lastname': 'test'
285 'lastname': 'test'
284 }
286 }
285 )
287 )
286 assertr = response.assert_response()
288 assertr = response.assert_response()
287 msg = u'This e-mail address is already taken'
289 msg = u'This e-mail address is already taken'
288 assertr.element_contains('#email+.error-message', msg)
290 assertr.element_contains('#email+.error-message', msg)
289
291
290 def test_register_err_wrong_data(self):
292 def test_register_err_wrong_data(self):
291 response = self.app.post(
293 response = self.app.post(
292 route_path('register'),
294 route_path('register'),
293 {
295 {
294 'username': 'xs',
296 'username': 'xs',
295 'password': 'test',
297 'password': 'test',
296 'password_confirmation': 'test',
298 'password_confirmation': 'test',
297 'email': 'goodmailm',
299 'email': 'goodmailm',
298 'firstname': 'test',
300 'firstname': 'test',
299 'lastname': 'test'
301 'lastname': 'test'
300 }
302 }
301 )
303 )
302 assert response.status == '200 OK'
304 assert response.status == '200 OK'
303 response.mustcontain('An email address must contain a single @')
305 response.mustcontain('An email address must contain a single @')
304 response.mustcontain('Enter a value 6 characters long or more')
306 response.mustcontain('Enter a value 6 characters long or more')
305
307
306 def test_register_err_username(self):
308 def test_register_err_username(self):
307 response = self.app.post(
309 response = self.app.post(
308 route_path('register'),
310 route_path('register'),
309 {
311 {
310 'username': 'error user',
312 'username': 'error user',
311 'password': 'test12',
313 'password': 'test12',
312 'password_confirmation': 'test12',
314 'password_confirmation': 'test12',
313 'email': 'goodmailm',
315 'email': 'goodmailm',
314 'firstname': 'test',
316 'firstname': 'test',
315 'lastname': 'test'
317 'lastname': 'test'
316 }
318 }
317 )
319 )
318
320
319 response.mustcontain('An email address must contain a single @')
321 response.mustcontain('An email address must contain a single @')
320 response.mustcontain(
322 response.mustcontain(
321 'Username may only contain '
323 'Username may only contain '
322 'alphanumeric characters underscores, '
324 'alphanumeric characters underscores, '
323 'periods or dashes and must begin with '
325 'periods or dashes and must begin with '
324 'alphanumeric character')
326 'alphanumeric character')
325
327
326 def test_register_err_case_sensitive(self):
328 def test_register_err_case_sensitive(self):
327 usr = 'Test_Admin'
329 usr = 'Test_Admin'
328 response = self.app.post(
330 response = self.app.post(
329 route_path('register'),
331 route_path('register'),
330 {
332 {
331 'username': usr,
333 'username': usr,
332 'password': 'test12',
334 'password': 'test12',
333 'password_confirmation': 'test12',
335 'password_confirmation': 'test12',
334 'email': 'goodmailm',
336 'email': 'goodmailm',
335 'firstname': 'test',
337 'firstname': 'test',
336 'lastname': 'test'
338 'lastname': 'test'
337 }
339 }
338 )
340 )
339
341
340 assertr = response.assert_response()
342 assertr = response.assert_response()
341 msg = u'Username "%(username)s" already exists'
343 msg = u'Username "%(username)s" already exists'
342 msg = msg % {'username': usr}
344 msg = msg % {'username': usr}
343 assertr.element_contains('#username+.error-message', msg)
345 assertr.element_contains('#username+.error-message', msg)
344
346
345 def test_register_special_chars(self):
347 def test_register_special_chars(self):
346 response = self.app.post(
348 response = self.app.post(
347 route_path('register'),
349 route_path('register'),
348 {
350 {
349 'username': 'xxxaxn',
351 'username': 'xxxaxn',
350 'password': 'ąćźżąśśśś',
352 'password': 'ąćźżąśśśś',
351 'password_confirmation': 'ąćźżąśśśś',
353 'password_confirmation': 'ąćźżąśśśś',
352 'email': 'goodmailm@test.plx',
354 'email': 'goodmailm@test.plx',
353 'firstname': 'test',
355 'firstname': 'test',
354 'lastname': 'test'
356 'lastname': 'test'
355 }
357 }
356 )
358 )
357
359
358 msg = u'Invalid characters (non-ascii) in password'
360 msg = u'Invalid characters (non-ascii) in password'
359 response.mustcontain(msg)
361 response.mustcontain(msg)
360
362
361 def test_register_password_mismatch(self):
363 def test_register_password_mismatch(self):
362 response = self.app.post(
364 response = self.app.post(
363 route_path('register'),
365 route_path('register'),
364 {
366 {
365 'username': 'xs',
367 'username': 'xs',
366 'password': '123qwe',
368 'password': '123qwe',
367 'password_confirmation': 'qwe123',
369 'password_confirmation': 'qwe123',
368 'email': 'goodmailm@test.plxa',
370 'email': 'goodmailm@test.plxa',
369 'firstname': 'test',
371 'firstname': 'test',
370 'lastname': 'test'
372 'lastname': 'test'
371 }
373 }
372 )
374 )
373 msg = u'Passwords do not match'
375 msg = u'Passwords do not match'
374 response.mustcontain(msg)
376 response.mustcontain(msg)
375
377
376 def test_register_ok(self):
378 def test_register_ok(self):
377 username = 'test_regular4'
379 username = 'test_regular4'
378 password = 'qweqwe'
380 password = 'qweqwe'
379 email = 'marcin@test.com'
381 email = 'marcin@test.com'
380 name = 'testname'
382 name = 'testname'
381 lastname = 'testlastname'
383 lastname = 'testlastname'
382
384
383 # this initializes a session
385 # this initializes a session
384 response = self.app.get(route_path('register'))
386 response = self.app.get(route_path('register'))
385 response.mustcontain('Create an Account')
387 response.mustcontain('Create an Account')
386
388
387
389
388 response = self.app.post(
390 response = self.app.post(
389 route_path('register'),
391 route_path('register'),
390 {
392 {
391 'username': username,
393 'username': username,
392 'password': password,
394 'password': password,
393 'password_confirmation': password,
395 'password_confirmation': password,
394 'email': email,
396 'email': email,
395 'firstname': name,
397 'firstname': name,
396 'lastname': lastname,
398 'lastname': lastname,
397 'admin': True
399 'admin': True
398 },
400 },
399 status=302
401 status=302
400 ) # This should be overridden
402 ) # This should be overridden
401
403
402 assert_session_flash(
404 assert_session_flash(
403 response, 'You have successfully registered with RhodeCode. You can log-in now.')
405 response, 'You have successfully registered with RhodeCode. You can log-in now.')
404
406
405 ret = Session().query(User).filter(
407 ret = Session().query(User).filter(
406 User.username == 'test_regular4').one()
408 User.username == 'test_regular4').one()
407 assert ret.username == username
409 assert ret.username == username
408 assert check_password(password, ret.password)
410 assert check_password(password, ret.password)
409 assert ret.email == email
411 assert ret.email == email
410 assert ret.name == name
412 assert ret.name == name
411 assert ret.lastname == lastname
413 assert ret.lastname == lastname
412 assert ret.auth_tokens is not None
414 assert ret.auth_tokens is not None
413 assert not ret.admin
415 assert not ret.admin
414
416
415 def test_forgot_password_wrong_mail(self):
417 def test_forgot_password_wrong_mail(self):
416 bad_email = 'marcin@wrongmail.org'
418 bad_email = 'marcin@wrongmail.org'
417 # this initializes a session
419 # this initializes a session
418 self.app.get(route_path('reset_password'))
420 self.app.get(route_path('reset_password'))
419
421
420 response = self.app.post(
422 response = self.app.post(
421 route_path('reset_password'), {'email': bad_email, }
423 route_path('reset_password'), {'email': bad_email, }
422 )
424 )
423 assert_session_flash(response,
425 assert_session_flash(response,
424 'If such email exists, a password reset link was sent to it.')
426 'If such email exists, a password reset link was sent to it.')
425
427
426 def test_forgot_password(self, user_util):
428 def test_forgot_password(self, user_util):
427 # this initializes a session
429 # this initializes a session
428 self.app.get(route_path('reset_password'))
430 self.app.get(route_path('reset_password'))
429
431
430 user = user_util.create_user()
432 user = user_util.create_user()
431 user_id = user.user_id
433 user_id = user.user_id
432 email = user.email
434 email = user.email
433
435
434 response = self.app.post(route_path('reset_password'), {'email': email, })
436 response = self.app.post(route_path('reset_password'), {'email': email, })
435
437
436 assert_session_flash(response,
438 assert_session_flash(response,
437 'If such email exists, a password reset link was sent to it.')
439 'If such email exists, a password reset link was sent to it.')
438
440
439 # BAD KEY
441 # BAD KEY
440 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
442 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
441 response = self.app.get(confirm_url, status=302)
443 response = self.app.get(confirm_url, status=302)
442 assert response.location.endswith(route_path('reset_password'))
444 assert response.location.endswith(route_path('reset_password'))
443 assert_session_flash(response, 'Given reset token is invalid')
445 assert_session_flash(response, 'Given reset token is invalid')
444
446
445 response.follow() # cleanup flash
447 response.follow() # cleanup flash
446
448
447 # GOOD KEY
449 # GOOD KEY
448 key = UserApiKeys.query()\
450 key = UserApiKeys.query()\
449 .filter(UserApiKeys.user_id == user_id)\
451 .filter(UserApiKeys.user_id == user_id)\
450 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
452 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
451 .first()
453 .first()
452
454
453 assert key
455 assert key
454
456
455 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
457 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
456 response = self.app.get(confirm_url)
458 response = self.app.get(confirm_url)
457 assert response.status == '302 Found'
459 assert response.status == '302 Found'
458 assert response.location.endswith(route_path('login'))
460 assert response.location.endswith(route_path('login'))
459
461
460 assert_session_flash(
462 assert_session_flash(
461 response,
463 response,
462 'Your password reset was successful, '
464 'Your password reset was successful, '
463 'a new password has been sent to your email')
465 'a new password has been sent to your email')
464
466
465 response.follow()
467 response.follow()
466
468
467 def _get_api_whitelist(self, values=None):
469 def _get_api_whitelist(self, values=None):
468 config = {'api_access_controllers_whitelist': values or []}
470 config = {'api_access_controllers_whitelist': values or []}
469 return config
471 return config
470
472
471 @pytest.mark.parametrize("test_name, auth_token", [
473 @pytest.mark.parametrize("test_name, auth_token", [
472 ('none', None),
474 ('none', None),
473 ('empty_string', ''),
475 ('empty_string', ''),
474 ('fake_number', '123456'),
476 ('fake_number', '123456'),
475 ('proper_auth_token', None)
477 ('proper_auth_token', None)
476 ])
478 ])
477 def test_access_not_whitelisted_page_via_auth_token(
479 def test_access_not_whitelisted_page_via_auth_token(
478 self, test_name, auth_token, user_admin):
480 self, test_name, auth_token, user_admin):
479
481
480 whitelist = self._get_api_whitelist([])
482 whitelist = self._get_api_whitelist([])
481 with mock.patch.dict('rhodecode.CONFIG', whitelist):
483 with mock.patch.dict('rhodecode.CONFIG', whitelist):
482 assert [] == whitelist['api_access_controllers_whitelist']
484 assert [] == whitelist['api_access_controllers_whitelist']
483 if test_name == 'proper_auth_token':
485 if test_name == 'proper_auth_token':
484 # use builtin if api_key is None
486 # use builtin if api_key is None
485 auth_token = user_admin.api_key
487 auth_token = user_admin.api_key
486
488
487 with fixture.anon_access(False):
489 with fixture.anon_access(False):
488 self.app.get(
490 self.app.get(
489 route_path('repo_commit_raw',
491 route_path('repo_commit_raw',
490 repo_name=HG_REPO, commit_id='tip',
492 repo_name=HG_REPO, commit_id='tip',
491 params=dict(api_key=auth_token)),
493 params=dict(api_key=auth_token)),
492 status=302)
494 status=302)
493
495
494 @pytest.mark.parametrize("test_name, auth_token, code", [
496 @pytest.mark.parametrize("test_name, auth_token, code", [
495 ('none', None, 302),
497 ('none', None, 302),
496 ('empty_string', '', 302),
498 ('empty_string', '', 302),
497 ('fake_number', '123456', 302),
499 ('fake_number', '123456', 302),
498 ('proper_auth_token', None, 200)
500 ('proper_auth_token', None, 200)
499 ])
501 ])
500 def test_access_whitelisted_page_via_auth_token(
502 def test_access_whitelisted_page_via_auth_token(
501 self, test_name, auth_token, code, user_admin):
503 self, test_name, auth_token, code, user_admin):
502
504
503 whitelist = self._get_api_whitelist(whitelist_view)
505 whitelist = self._get_api_whitelist(whitelist_view)
504
506
505 with mock.patch.dict('rhodecode.CONFIG', whitelist):
507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
506 assert whitelist_view == whitelist['api_access_controllers_whitelist']
508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
507
509
508 if test_name == 'proper_auth_token':
510 if test_name == 'proper_auth_token':
509 auth_token = user_admin.api_key
511 auth_token = user_admin.api_key
510 assert auth_token
512 assert auth_token
511
513
512 with fixture.anon_access(False):
514 with fixture.anon_access(False):
513 self.app.get(
515 self.app.get(
514 route_path('repo_commit_raw',
516 route_path('repo_commit_raw',
515 repo_name=HG_REPO, commit_id='tip',
517 repo_name=HG_REPO, commit_id='tip',
516 params=dict(api_key=auth_token)),
518 params=dict(api_key=auth_token)),
517 status=code)
519 status=code)
518
520
519 @pytest.mark.parametrize("test_name, auth_token, code", [
521 @pytest.mark.parametrize("test_name, auth_token, code", [
520 ('proper_auth_token', None, 200),
522 ('proper_auth_token', None, 200),
521 ('wrong_auth_token', '123456', 302),
523 ('wrong_auth_token', '123456', 302),
522 ])
524 ])
523 def test_access_whitelisted_page_via_auth_token_bound_to_token(
525 def test_access_whitelisted_page_via_auth_token_bound_to_token(
524 self, test_name, auth_token, code, user_admin):
526 self, test_name, auth_token, code, user_admin):
525
527
526 expected_token = auth_token
528 expected_token = auth_token
527 if test_name == 'proper_auth_token':
529 if test_name == 'proper_auth_token':
528 auth_token = user_admin.api_key
530 auth_token = user_admin.api_key
529 expected_token = auth_token
531 expected_token = auth_token
530 assert auth_token
532 assert auth_token
531
533
532 whitelist = self._get_api_whitelist([
534 whitelist = self._get_api_whitelist([
533 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
535 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
534
536
535 with mock.patch.dict('rhodecode.CONFIG', whitelist):
537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
536
538
537 with fixture.anon_access(False):
539 with fixture.anon_access(False):
538 self.app.get(
540 self.app.get(
539 route_path('repo_commit_raw',
541 route_path('repo_commit_raw',
540 repo_name=HG_REPO, commit_id='tip',
542 repo_name=HG_REPO, commit_id='tip',
541 params=dict(api_key=auth_token)),
543 params=dict(api_key=auth_token)),
542 status=code)
544 status=code)
543
545
544 def test_access_page_via_extra_auth_token(self):
546 def test_access_page_via_extra_auth_token(self):
545 whitelist = self._get_api_whitelist(whitelist_view)
547 whitelist = self._get_api_whitelist(whitelist_view)
546 with mock.patch.dict('rhodecode.CONFIG', whitelist):
548 with mock.patch.dict('rhodecode.CONFIG', whitelist):
547 assert whitelist_view == \
549 assert whitelist_view == \
548 whitelist['api_access_controllers_whitelist']
550 whitelist['api_access_controllers_whitelist']
549
551
550 new_auth_token = AuthTokenModel().create(
552 new_auth_token = AuthTokenModel().create(
551 TEST_USER_ADMIN_LOGIN, 'test')
553 TEST_USER_ADMIN_LOGIN, 'test')
552 Session().commit()
554 Session().commit()
553 with fixture.anon_access(False):
555 with fixture.anon_access(False):
554 self.app.get(
556 self.app.get(
555 route_path('repo_commit_raw',
557 route_path('repo_commit_raw',
556 repo_name=HG_REPO, commit_id='tip',
558 repo_name=HG_REPO, commit_id='tip',
557 params=dict(api_key=new_auth_token.api_key)),
559 params=dict(api_key=new_auth_token.api_key)),
558 status=200)
560 status=200)
559
561
560 def test_access_page_via_expired_auth_token(self):
562 def test_access_page_via_expired_auth_token(self):
561 whitelist = self._get_api_whitelist(whitelist_view)
563 whitelist = self._get_api_whitelist(whitelist_view)
562 with mock.patch.dict('rhodecode.CONFIG', whitelist):
564 with mock.patch.dict('rhodecode.CONFIG', whitelist):
563 assert whitelist_view == \
565 assert whitelist_view == \
564 whitelist['api_access_controllers_whitelist']
566 whitelist['api_access_controllers_whitelist']
565
567
566 new_auth_token = AuthTokenModel().create(
568 new_auth_token = AuthTokenModel().create(
567 TEST_USER_ADMIN_LOGIN, 'test')
569 TEST_USER_ADMIN_LOGIN, 'test')
568 Session().commit()
570 Session().commit()
569 # patch the api key and make it expired
571 # patch the api key and make it expired
570 new_auth_token.expires = 0
572 new_auth_token.expires = 0
571 Session().add(new_auth_token)
573 Session().add(new_auth_token)
572 Session().commit()
574 Session().commit()
573 with fixture.anon_access(False):
575 with fixture.anon_access(False):
574 self.app.get(
576 self.app.get(
575 route_path('repo_commit_raw',
577 route_path('repo_commit_raw',
576 repo_name=HG_REPO, commit_id='tip',
578 repo_name=HG_REPO, commit_id='tip',
577 params=dict(api_key=new_auth_token.api_key)),
579 params=dict(api_key=new_auth_token.api_key)),
578 status=302)
580 status=302)
@@ -1,800 +1,801 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode import forms
32 from rhodecode import forms
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, NotAnonymous, CSRFRequired,
37 LoginRequired, NotAnonymous, CSRFRequired,
38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 from rhodecode.lib.channelstream import (
39 from rhodecode.lib.channelstream import (
40 channelstream_request, ChannelstreamException)
40 channelstream_request, ChannelstreamException)
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
43 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 IntegrityError, or_, in_filter_generator,
45 IntegrityError, or_, in_filter_generator,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 PullRequest, UserBookmark, RepoGroup)
47 PullRequest, UserBookmark, RepoGroup)
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.validation_schema.schemas import user_schema
52 from rhodecode.model.validation_schema.schemas import user_schema
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class MyAccountView(BaseAppView, DataGridAppView):
57 class MyAccountView(BaseAppView, DataGridAppView):
58 ALLOW_SCOPED_TOKENS = False
58 ALLOW_SCOPED_TOKENS = False
59 """
59 """
60 This view has alternative version inside EE, if modified please take a look
60 This view has alternative version inside EE, if modified please take a look
61 in there as well.
61 in there as well.
62 """
62 """
63
63
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context()
65 c = self._get_local_tmpl_context()
66 c.user = c.auth_user.get_instance()
66 c.user = c.auth_user.get_instance()
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68
68
69 return c
69 return c
70
70
71 @LoginRequired()
71 @LoginRequired()
72 @NotAnonymous()
72 @NotAnonymous()
73 @view_config(
73 @view_config(
74 route_name='my_account_profile', request_method='GET',
74 route_name='my_account_profile', request_method='GET',
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 def my_account_profile(self):
76 def my_account_profile(self):
77 c = self.load_default_context()
77 c = self.load_default_context()
78 c.active = 'profile'
78 c.active = 'profile'
79 c.extern_type = c.user.extern_type
79 return self._get_template_context(c)
80 return self._get_template_context(c)
80
81
81 @LoginRequired()
82 @LoginRequired()
82 @NotAnonymous()
83 @NotAnonymous()
83 @view_config(
84 @view_config(
84 route_name='my_account_password', request_method='GET',
85 route_name='my_account_password', request_method='GET',
85 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 def my_account_password(self):
87 def my_account_password(self):
87 c = self.load_default_context()
88 c = self.load_default_context()
88 c.active = 'password'
89 c.active = 'password'
89 c.extern_type = c.user.extern_type
90 c.extern_type = c.user.extern_type
90
91
91 schema = user_schema.ChangePasswordSchema().bind(
92 schema = user_schema.ChangePasswordSchema().bind(
92 username=c.user.username)
93 username=c.user.username)
93
94
94 form = forms.Form(
95 form = forms.Form(
95 schema,
96 schema,
96 action=h.route_path('my_account_password_update'),
97 action=h.route_path('my_account_password_update'),
97 buttons=(forms.buttons.save, forms.buttons.reset))
98 buttons=(forms.buttons.save, forms.buttons.reset))
98
99
99 c.form = form
100 c.form = form
100 return self._get_template_context(c)
101 return self._get_template_context(c)
101
102
102 @LoginRequired()
103 @LoginRequired()
103 @NotAnonymous()
104 @NotAnonymous()
104 @CSRFRequired()
105 @CSRFRequired()
105 @view_config(
106 @view_config(
106 route_name='my_account_password_update', request_method='POST',
107 route_name='my_account_password_update', request_method='POST',
107 renderer='rhodecode:templates/admin/my_account/my_account.mako')
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
108 def my_account_password_update(self):
109 def my_account_password_update(self):
109 _ = self.request.translate
110 _ = self.request.translate
110 c = self.load_default_context()
111 c = self.load_default_context()
111 c.active = 'password'
112 c.active = 'password'
112 c.extern_type = c.user.extern_type
113 c.extern_type = c.user.extern_type
113
114
114 schema = user_schema.ChangePasswordSchema().bind(
115 schema = user_schema.ChangePasswordSchema().bind(
115 username=c.user.username)
116 username=c.user.username)
116
117
117 form = forms.Form(
118 form = forms.Form(
118 schema, buttons=(forms.buttons.save, forms.buttons.reset))
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
119
120
120 if c.extern_type != 'rhodecode':
121 if c.extern_type != 'rhodecode':
121 raise HTTPFound(self.request.route_path('my_account_password'))
122 raise HTTPFound(self.request.route_path('my_account_password'))
122
123
123 controls = self.request.POST.items()
124 controls = self.request.POST.items()
124 try:
125 try:
125 valid_data = form.validate(controls)
126 valid_data = form.validate(controls)
126 UserModel().update_user(c.user.user_id, **valid_data)
127 UserModel().update_user(c.user.user_id, **valid_data)
127 c.user.update_userdata(force_password_change=False)
128 c.user.update_userdata(force_password_change=False)
128 Session().commit()
129 Session().commit()
129 except forms.ValidationFailure as e:
130 except forms.ValidationFailure as e:
130 c.form = e
131 c.form = e
131 return self._get_template_context(c)
132 return self._get_template_context(c)
132
133
133 except Exception:
134 except Exception:
134 log.exception("Exception updating password")
135 log.exception("Exception updating password")
135 h.flash(_('Error occurred during update of user password'),
136 h.flash(_('Error occurred during update of user password'),
136 category='error')
137 category='error')
137 else:
138 else:
138 instance = c.auth_user.get_instance()
139 instance = c.auth_user.get_instance()
139 self.session.setdefault('rhodecode_user', {}).update(
140 self.session.setdefault('rhodecode_user', {}).update(
140 {'password': md5(instance.password)})
141 {'password': md5(instance.password)})
141 self.session.save()
142 self.session.save()
142 h.flash(_("Successfully updated password"), category='success')
143 h.flash(_("Successfully updated password"), category='success')
143
144
144 raise HTTPFound(self.request.route_path('my_account_password'))
145 raise HTTPFound(self.request.route_path('my_account_password'))
145
146
146 @LoginRequired()
147 @LoginRequired()
147 @NotAnonymous()
148 @NotAnonymous()
148 @view_config(
149 @view_config(
149 route_name='my_account_auth_tokens', request_method='GET',
150 route_name='my_account_auth_tokens', request_method='GET',
150 renderer='rhodecode:templates/admin/my_account/my_account.mako')
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
151 def my_account_auth_tokens(self):
152 def my_account_auth_tokens(self):
152 _ = self.request.translate
153 _ = self.request.translate
153
154
154 c = self.load_default_context()
155 c = self.load_default_context()
155 c.active = 'auth_tokens'
156 c.active = 'auth_tokens'
156 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
157 c.role_values = [
158 c.role_values = [
158 (x, AuthTokenModel.cls._get_role_name(x))
159 (x, AuthTokenModel.cls._get_role_name(x))
159 for x in AuthTokenModel.cls.ROLES]
160 for x in AuthTokenModel.cls.ROLES]
160 c.role_options = [(c.role_values, _("Role"))]
161 c.role_options = [(c.role_values, _("Role"))]
161 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
162 c.user.user_id, show_expired=True)
163 c.user.user_id, show_expired=True)
163 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
164 return self._get_template_context(c)
165 return self._get_template_context(c)
165
166
166 def maybe_attach_token_scope(self, token):
167 def maybe_attach_token_scope(self, token):
167 # implemented in EE edition
168 # implemented in EE edition
168 pass
169 pass
169
170
170 @LoginRequired()
171 @LoginRequired()
171 @NotAnonymous()
172 @NotAnonymous()
172 @CSRFRequired()
173 @CSRFRequired()
173 @view_config(
174 @view_config(
174 route_name='my_account_auth_tokens_add', request_method='POST',)
175 route_name='my_account_auth_tokens_add', request_method='POST',)
175 def my_account_auth_tokens_add(self):
176 def my_account_auth_tokens_add(self):
176 _ = self.request.translate
177 _ = self.request.translate
177 c = self.load_default_context()
178 c = self.load_default_context()
178
179
179 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
180 description = self.request.POST.get('description')
181 description = self.request.POST.get('description')
181 role = self.request.POST.get('role')
182 role = self.request.POST.get('role')
182
183
183 token = UserModel().add_auth_token(
184 token = UserModel().add_auth_token(
184 user=c.user.user_id,
185 user=c.user.user_id,
185 lifetime_minutes=lifetime, role=role, description=description,
186 lifetime_minutes=lifetime, role=role, description=description,
186 scope_callback=self.maybe_attach_token_scope)
187 scope_callback=self.maybe_attach_token_scope)
187 token_data = token.get_api_data()
188 token_data = token.get_api_data()
188
189
189 audit_logger.store_web(
190 audit_logger.store_web(
190 'user.edit.token.add', action_data={
191 'user.edit.token.add', action_data={
191 'data': {'token': token_data, 'user': 'self'}},
192 'data': {'token': token_data, 'user': 'self'}},
192 user=self._rhodecode_user, )
193 user=self._rhodecode_user, )
193 Session().commit()
194 Session().commit()
194
195
195 h.flash(_("Auth token successfully created"), category='success')
196 h.flash(_("Auth token successfully created"), category='success')
196 return HTTPFound(h.route_path('my_account_auth_tokens'))
197 return HTTPFound(h.route_path('my_account_auth_tokens'))
197
198
198 @LoginRequired()
199 @LoginRequired()
199 @NotAnonymous()
200 @NotAnonymous()
200 @CSRFRequired()
201 @CSRFRequired()
201 @view_config(
202 @view_config(
202 route_name='my_account_auth_tokens_delete', request_method='POST')
203 route_name='my_account_auth_tokens_delete', request_method='POST')
203 def my_account_auth_tokens_delete(self):
204 def my_account_auth_tokens_delete(self):
204 _ = self.request.translate
205 _ = self.request.translate
205 c = self.load_default_context()
206 c = self.load_default_context()
206
207
207 del_auth_token = self.request.POST.get('del_auth_token')
208 del_auth_token = self.request.POST.get('del_auth_token')
208
209
209 if del_auth_token:
210 if del_auth_token:
210 token = UserApiKeys.get_or_404(del_auth_token)
211 token = UserApiKeys.get_or_404(del_auth_token)
211 token_data = token.get_api_data()
212 token_data = token.get_api_data()
212
213
213 AuthTokenModel().delete(del_auth_token, c.user.user_id)
214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
214 audit_logger.store_web(
215 audit_logger.store_web(
215 'user.edit.token.delete', action_data={
216 'user.edit.token.delete', action_data={
216 'data': {'token': token_data, 'user': 'self'}},
217 'data': {'token': token_data, 'user': 'self'}},
217 user=self._rhodecode_user,)
218 user=self._rhodecode_user,)
218 Session().commit()
219 Session().commit()
219 h.flash(_("Auth token successfully deleted"), category='success')
220 h.flash(_("Auth token successfully deleted"), category='success')
220
221
221 return HTTPFound(h.route_path('my_account_auth_tokens'))
222 return HTTPFound(h.route_path('my_account_auth_tokens'))
222
223
223 @LoginRequired()
224 @LoginRequired()
224 @NotAnonymous()
225 @NotAnonymous()
225 @view_config(
226 @view_config(
226 route_name='my_account_emails', request_method='GET',
227 route_name='my_account_emails', request_method='GET',
227 renderer='rhodecode:templates/admin/my_account/my_account.mako')
228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
228 def my_account_emails(self):
229 def my_account_emails(self):
229 _ = self.request.translate
230 _ = self.request.translate
230
231
231 c = self.load_default_context()
232 c = self.load_default_context()
232 c.active = 'emails'
233 c.active = 'emails'
233
234
234 c.user_email_map = UserEmailMap.query()\
235 c.user_email_map = UserEmailMap.query()\
235 .filter(UserEmailMap.user == c.user).all()
236 .filter(UserEmailMap.user == c.user).all()
236
237
237 schema = user_schema.AddEmailSchema().bind(
238 schema = user_schema.AddEmailSchema().bind(
238 username=c.user.username, user_emails=c.user.emails)
239 username=c.user.username, user_emails=c.user.emails)
239
240
240 form = forms.RcForm(schema,
241 form = forms.RcForm(schema,
241 action=h.route_path('my_account_emails_add'),
242 action=h.route_path('my_account_emails_add'),
242 buttons=(forms.buttons.save, forms.buttons.reset))
243 buttons=(forms.buttons.save, forms.buttons.reset))
243
244
244 c.form = form
245 c.form = form
245 return self._get_template_context(c)
246 return self._get_template_context(c)
246
247
247 @LoginRequired()
248 @LoginRequired()
248 @NotAnonymous()
249 @NotAnonymous()
249 @CSRFRequired()
250 @CSRFRequired()
250 @view_config(
251 @view_config(
251 route_name='my_account_emails_add', request_method='POST',
252 route_name='my_account_emails_add', request_method='POST',
252 renderer='rhodecode:templates/admin/my_account/my_account.mako')
253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
253 def my_account_emails_add(self):
254 def my_account_emails_add(self):
254 _ = self.request.translate
255 _ = self.request.translate
255 c = self.load_default_context()
256 c = self.load_default_context()
256 c.active = 'emails'
257 c.active = 'emails'
257
258
258 schema = user_schema.AddEmailSchema().bind(
259 schema = user_schema.AddEmailSchema().bind(
259 username=c.user.username, user_emails=c.user.emails)
260 username=c.user.username, user_emails=c.user.emails)
260
261
261 form = forms.RcForm(
262 form = forms.RcForm(
262 schema, action=h.route_path('my_account_emails_add'),
263 schema, action=h.route_path('my_account_emails_add'),
263 buttons=(forms.buttons.save, forms.buttons.reset))
264 buttons=(forms.buttons.save, forms.buttons.reset))
264
265
265 controls = self.request.POST.items()
266 controls = self.request.POST.items()
266 try:
267 try:
267 valid_data = form.validate(controls)
268 valid_data = form.validate(controls)
268 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
269 audit_logger.store_web(
270 audit_logger.store_web(
270 'user.edit.email.add', action_data={
271 'user.edit.email.add', action_data={
271 'data': {'email': valid_data['email'], 'user': 'self'}},
272 'data': {'email': valid_data['email'], 'user': 'self'}},
272 user=self._rhodecode_user,)
273 user=self._rhodecode_user,)
273 Session().commit()
274 Session().commit()
274 except formencode.Invalid as error:
275 except formencode.Invalid as error:
275 h.flash(h.escape(error.error_dict['email']), category='error')
276 h.flash(h.escape(error.error_dict['email']), category='error')
276 except forms.ValidationFailure as e:
277 except forms.ValidationFailure as e:
277 c.user_email_map = UserEmailMap.query() \
278 c.user_email_map = UserEmailMap.query() \
278 .filter(UserEmailMap.user == c.user).all()
279 .filter(UserEmailMap.user == c.user).all()
279 c.form = e
280 c.form = e
280 return self._get_template_context(c)
281 return self._get_template_context(c)
281 except Exception:
282 except Exception:
282 log.exception("Exception adding email")
283 log.exception("Exception adding email")
283 h.flash(_('Error occurred during adding email'),
284 h.flash(_('Error occurred during adding email'),
284 category='error')
285 category='error')
285 else:
286 else:
286 h.flash(_("Successfully added email"), category='success')
287 h.flash(_("Successfully added email"), category='success')
287
288
288 raise HTTPFound(self.request.route_path('my_account_emails'))
289 raise HTTPFound(self.request.route_path('my_account_emails'))
289
290
290 @LoginRequired()
291 @LoginRequired()
291 @NotAnonymous()
292 @NotAnonymous()
292 @CSRFRequired()
293 @CSRFRequired()
293 @view_config(
294 @view_config(
294 route_name='my_account_emails_delete', request_method='POST')
295 route_name='my_account_emails_delete', request_method='POST')
295 def my_account_emails_delete(self):
296 def my_account_emails_delete(self):
296 _ = self.request.translate
297 _ = self.request.translate
297 c = self.load_default_context()
298 c = self.load_default_context()
298
299
299 del_email_id = self.request.POST.get('del_email_id')
300 del_email_id = self.request.POST.get('del_email_id')
300 if del_email_id:
301 if del_email_id:
301 email = UserEmailMap.get_or_404(del_email_id).email
302 email = UserEmailMap.get_or_404(del_email_id).email
302 UserModel().delete_extra_email(c.user.user_id, del_email_id)
303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
303 audit_logger.store_web(
304 audit_logger.store_web(
304 'user.edit.email.delete', action_data={
305 'user.edit.email.delete', action_data={
305 'data': {'email': email, 'user': 'self'}},
306 'data': {'email': email, 'user': 'self'}},
306 user=self._rhodecode_user,)
307 user=self._rhodecode_user,)
307 Session().commit()
308 Session().commit()
308 h.flash(_("Email successfully deleted"),
309 h.flash(_("Email successfully deleted"),
309 category='success')
310 category='success')
310 return HTTPFound(h.route_path('my_account_emails'))
311 return HTTPFound(h.route_path('my_account_emails'))
311
312
312 @LoginRequired()
313 @LoginRequired()
313 @NotAnonymous()
314 @NotAnonymous()
314 @CSRFRequired()
315 @CSRFRequired()
315 @view_config(
316 @view_config(
316 route_name='my_account_notifications_test_channelstream',
317 route_name='my_account_notifications_test_channelstream',
317 request_method='POST', renderer='json_ext')
318 request_method='POST', renderer='json_ext')
318 def my_account_notifications_test_channelstream(self):
319 def my_account_notifications_test_channelstream(self):
319 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
320 self._rhodecode_user.username, datetime.datetime.now())
321 self._rhodecode_user.username, datetime.datetime.now())
321 payload = {
322 payload = {
322 # 'channel': 'broadcast',
323 # 'channel': 'broadcast',
323 'type': 'message',
324 'type': 'message',
324 'timestamp': datetime.datetime.utcnow(),
325 'timestamp': datetime.datetime.utcnow(),
325 'user': 'system',
326 'user': 'system',
326 'pm_users': [self._rhodecode_user.username],
327 'pm_users': [self._rhodecode_user.username],
327 'message': {
328 'message': {
328 'message': message,
329 'message': message,
329 'level': 'info',
330 'level': 'info',
330 'topic': '/notifications'
331 'topic': '/notifications'
331 }
332 }
332 }
333 }
333
334
334 registry = self.request.registry
335 registry = self.request.registry
335 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
336 channelstream_config = rhodecode_plugins.get('channelstream', {})
337 channelstream_config = rhodecode_plugins.get('channelstream', {})
337
338
338 try:
339 try:
339 channelstream_request(channelstream_config, [payload], '/message')
340 channelstream_request(channelstream_config, [payload], '/message')
340 except ChannelstreamException as e:
341 except ChannelstreamException as e:
341 log.exception('Failed to send channelstream data')
342 log.exception('Failed to send channelstream data')
342 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
343 return {"response": 'Channelstream data sent. '
344 return {"response": 'Channelstream data sent. '
344 'You should see a new live message now.'}
345 'You should see a new live message now.'}
345
346
346 def _load_my_repos_data(self, watched=False):
347 def _load_my_repos_data(self, watched=False):
347
348
348 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
349 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
349
350
350 if watched:
351 if watched:
351 # repos user watch
352 # repos user watch
352 repo_list = Session().query(
353 repo_list = Session().query(
353 Repository
354 Repository
354 ) \
355 ) \
355 .join(
356 .join(
356 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
357 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
357 ) \
358 ) \
358 .filter(
359 .filter(
359 UserFollowing.user_id == self._rhodecode_user.user_id
360 UserFollowing.user_id == self._rhodecode_user.user_id
360 ) \
361 ) \
361 .filter(or_(
362 .filter(or_(
362 # generate multiple IN to fix limitation problems
363 # generate multiple IN to fix limitation problems
363 *in_filter_generator(Repository.repo_id, allowed_ids))
364 *in_filter_generator(Repository.repo_id, allowed_ids))
364 ) \
365 ) \
365 .order_by(Repository.repo_name) \
366 .order_by(Repository.repo_name) \
366 .all()
367 .all()
367
368
368 else:
369 else:
369 # repos user is owner of
370 # repos user is owner of
370 repo_list = Session().query(
371 repo_list = Session().query(
371 Repository
372 Repository
372 ) \
373 ) \
373 .filter(
374 .filter(
374 Repository.user_id == self._rhodecode_user.user_id
375 Repository.user_id == self._rhodecode_user.user_id
375 ) \
376 ) \
376 .filter(or_(
377 .filter(or_(
377 # generate multiple IN to fix limitation problems
378 # generate multiple IN to fix limitation problems
378 *in_filter_generator(Repository.repo_id, allowed_ids))
379 *in_filter_generator(Repository.repo_id, allowed_ids))
379 ) \
380 ) \
380 .order_by(Repository.repo_name) \
381 .order_by(Repository.repo_name) \
381 .all()
382 .all()
382
383
383 _render = self.request.get_partial_renderer(
384 _render = self.request.get_partial_renderer(
384 'rhodecode:templates/data_table/_dt_elements.mako')
385 'rhodecode:templates/data_table/_dt_elements.mako')
385
386
386 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
387 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
387 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
388 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
388 short_name=False, admin=False)
389 short_name=False, admin=False)
389
390
390 repos_data = []
391 repos_data = []
391 for repo in repo_list:
392 for repo in repo_list:
392 row = {
393 row = {
393 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
394 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
394 repo.private, repo.archived, repo.fork),
395 repo.private, repo.archived, repo.fork),
395 "name_raw": repo.repo_name.lower(),
396 "name_raw": repo.repo_name.lower(),
396 }
397 }
397
398
398 repos_data.append(row)
399 repos_data.append(row)
399
400
400 # json used to render the grid
401 # json used to render the grid
401 return json.dumps(repos_data)
402 return json.dumps(repos_data)
402
403
403 @LoginRequired()
404 @LoginRequired()
404 @NotAnonymous()
405 @NotAnonymous()
405 @view_config(
406 @view_config(
406 route_name='my_account_repos', request_method='GET',
407 route_name='my_account_repos', request_method='GET',
407 renderer='rhodecode:templates/admin/my_account/my_account.mako')
408 renderer='rhodecode:templates/admin/my_account/my_account.mako')
408 def my_account_repos(self):
409 def my_account_repos(self):
409 c = self.load_default_context()
410 c = self.load_default_context()
410 c.active = 'repos'
411 c.active = 'repos'
411
412
412 # json used to render the grid
413 # json used to render the grid
413 c.data = self._load_my_repos_data()
414 c.data = self._load_my_repos_data()
414 return self._get_template_context(c)
415 return self._get_template_context(c)
415
416
416 @LoginRequired()
417 @LoginRequired()
417 @NotAnonymous()
418 @NotAnonymous()
418 @view_config(
419 @view_config(
419 route_name='my_account_watched', request_method='GET',
420 route_name='my_account_watched', request_method='GET',
420 renderer='rhodecode:templates/admin/my_account/my_account.mako')
421 renderer='rhodecode:templates/admin/my_account/my_account.mako')
421 def my_account_watched(self):
422 def my_account_watched(self):
422 c = self.load_default_context()
423 c = self.load_default_context()
423 c.active = 'watched'
424 c.active = 'watched'
424
425
425 # json used to render the grid
426 # json used to render the grid
426 c.data = self._load_my_repos_data(watched=True)
427 c.data = self._load_my_repos_data(watched=True)
427 return self._get_template_context(c)
428 return self._get_template_context(c)
428
429
429 @LoginRequired()
430 @LoginRequired()
430 @NotAnonymous()
431 @NotAnonymous()
431 @view_config(
432 @view_config(
432 route_name='my_account_bookmarks', request_method='GET',
433 route_name='my_account_bookmarks', request_method='GET',
433 renderer='rhodecode:templates/admin/my_account/my_account.mako')
434 renderer='rhodecode:templates/admin/my_account/my_account.mako')
434 def my_account_bookmarks(self):
435 def my_account_bookmarks(self):
435 c = self.load_default_context()
436 c = self.load_default_context()
436 c.active = 'bookmarks'
437 c.active = 'bookmarks'
437 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
438 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
438 self._rhodecode_db_user.user_id, cache=False)
439 self._rhodecode_db_user.user_id, cache=False)
439 return self._get_template_context(c)
440 return self._get_template_context(c)
440
441
441 def _process_bookmark_entry(self, entry, user_id):
442 def _process_bookmark_entry(self, entry, user_id):
442 position = safe_int(entry.get('position'))
443 position = safe_int(entry.get('position'))
443 cur_position = safe_int(entry.get('cur_position'))
444 cur_position = safe_int(entry.get('cur_position'))
444 if position is None:
445 if position is None:
445 return
446 return
446
447
447 # check if this is an existing entry
448 # check if this is an existing entry
448 is_new = False
449 is_new = False
449 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
450 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
450
451
451 if db_entry and str2bool(entry.get('remove')):
452 if db_entry and str2bool(entry.get('remove')):
452 log.debug('Marked bookmark %s for deletion', db_entry)
453 log.debug('Marked bookmark %s for deletion', db_entry)
453 Session().delete(db_entry)
454 Session().delete(db_entry)
454 return
455 return
455
456
456 if not db_entry:
457 if not db_entry:
457 # new
458 # new
458 db_entry = UserBookmark()
459 db_entry = UserBookmark()
459 is_new = True
460 is_new = True
460
461
461 should_save = False
462 should_save = False
462 default_redirect_url = ''
463 default_redirect_url = ''
463
464
464 # save repo
465 # save repo
465 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
466 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
466 repo = Repository.get(entry['bookmark_repo'])
467 repo = Repository.get(entry['bookmark_repo'])
467 perm_check = HasRepoPermissionAny(
468 perm_check = HasRepoPermissionAny(
468 'repository.read', 'repository.write', 'repository.admin')
469 'repository.read', 'repository.write', 'repository.admin')
469 if repo and perm_check(repo_name=repo.repo_name):
470 if repo and perm_check(repo_name=repo.repo_name):
470 db_entry.repository = repo
471 db_entry.repository = repo
471 should_save = True
472 should_save = True
472 default_redirect_url = '${repo_url}'
473 default_redirect_url = '${repo_url}'
473 # save repo group
474 # save repo group
474 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
475 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
475 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
476 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
476 perm_check = HasRepoGroupPermissionAny(
477 perm_check = HasRepoGroupPermissionAny(
477 'group.read', 'group.write', 'group.admin')
478 'group.read', 'group.write', 'group.admin')
478
479
479 if repo_group and perm_check(group_name=repo_group.group_name):
480 if repo_group and perm_check(group_name=repo_group.group_name):
480 db_entry.repository_group = repo_group
481 db_entry.repository_group = repo_group
481 should_save = True
482 should_save = True
482 default_redirect_url = '${repo_group_url}'
483 default_redirect_url = '${repo_group_url}'
483 # save generic info
484 # save generic info
484 elif entry.get('title') and entry.get('redirect_url'):
485 elif entry.get('title') and entry.get('redirect_url'):
485 should_save = True
486 should_save = True
486
487
487 if should_save:
488 if should_save:
488 # mark user and position
489 # mark user and position
489 db_entry.user_id = user_id
490 db_entry.user_id = user_id
490 db_entry.position = position
491 db_entry.position = position
491 db_entry.title = entry.get('title')
492 db_entry.title = entry.get('title')
492 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
493 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
493 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
494 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
494
495
495 Session().add(db_entry)
496 Session().add(db_entry)
496
497
497 @LoginRequired()
498 @LoginRequired()
498 @NotAnonymous()
499 @NotAnonymous()
499 @CSRFRequired()
500 @CSRFRequired()
500 @view_config(
501 @view_config(
501 route_name='my_account_bookmarks_update', request_method='POST')
502 route_name='my_account_bookmarks_update', request_method='POST')
502 def my_account_bookmarks_update(self):
503 def my_account_bookmarks_update(self):
503 _ = self.request.translate
504 _ = self.request.translate
504 c = self.load_default_context()
505 c = self.load_default_context()
505 c.active = 'bookmarks'
506 c.active = 'bookmarks'
506
507
507 controls = peppercorn.parse(self.request.POST.items())
508 controls = peppercorn.parse(self.request.POST.items())
508 user_id = c.user.user_id
509 user_id = c.user.user_id
509
510
510 # validate positions
511 # validate positions
511 positions = {}
512 positions = {}
512 for entry in controls.get('bookmarks', []):
513 for entry in controls.get('bookmarks', []):
513 position = safe_int(entry['position'])
514 position = safe_int(entry['position'])
514 if position is None:
515 if position is None:
515 continue
516 continue
516
517
517 if position in positions:
518 if position in positions:
518 h.flash(_("Position {} is defined twice. "
519 h.flash(_("Position {} is defined twice. "
519 "Please correct this error.").format(position), category='error')
520 "Please correct this error.").format(position), category='error')
520 return HTTPFound(h.route_path('my_account_bookmarks'))
521 return HTTPFound(h.route_path('my_account_bookmarks'))
521
522
522 entry['position'] = position
523 entry['position'] = position
523 entry['cur_position'] = safe_int(entry.get('cur_position'))
524 entry['cur_position'] = safe_int(entry.get('cur_position'))
524 positions[position] = entry
525 positions[position] = entry
525
526
526 try:
527 try:
527 for entry in positions.values():
528 for entry in positions.values():
528 self._process_bookmark_entry(entry, user_id)
529 self._process_bookmark_entry(entry, user_id)
529
530
530 Session().commit()
531 Session().commit()
531 h.flash(_("Update Bookmarks"), category='success')
532 h.flash(_("Update Bookmarks"), category='success')
532 except IntegrityError:
533 except IntegrityError:
533 h.flash(_("Failed to update bookmarks. "
534 h.flash(_("Failed to update bookmarks. "
534 "Make sure an unique position is used."), category='error')
535 "Make sure an unique position is used."), category='error')
535
536
536 return HTTPFound(h.route_path('my_account_bookmarks'))
537 return HTTPFound(h.route_path('my_account_bookmarks'))
537
538
538 @LoginRequired()
539 @LoginRequired()
539 @NotAnonymous()
540 @NotAnonymous()
540 @view_config(
541 @view_config(
541 route_name='my_account_goto_bookmark', request_method='GET',
542 route_name='my_account_goto_bookmark', request_method='GET',
542 renderer='rhodecode:templates/admin/my_account/my_account.mako')
543 renderer='rhodecode:templates/admin/my_account/my_account.mako')
543 def my_account_goto_bookmark(self):
544 def my_account_goto_bookmark(self):
544
545
545 bookmark_id = self.request.matchdict['bookmark_id']
546 bookmark_id = self.request.matchdict['bookmark_id']
546 user_bookmark = UserBookmark().query()\
547 user_bookmark = UserBookmark().query()\
547 .filter(UserBookmark.user_id == self.request.user.user_id) \
548 .filter(UserBookmark.user_id == self.request.user.user_id) \
548 .filter(UserBookmark.position == bookmark_id).scalar()
549 .filter(UserBookmark.position == bookmark_id).scalar()
549
550
550 redirect_url = h.route_path('my_account_bookmarks')
551 redirect_url = h.route_path('my_account_bookmarks')
551 if not user_bookmark:
552 if not user_bookmark:
552 raise HTTPFound(redirect_url)
553 raise HTTPFound(redirect_url)
553
554
554 # repository set
555 # repository set
555 if user_bookmark.repository:
556 if user_bookmark.repository:
556 repo_name = user_bookmark.repository.repo_name
557 repo_name = user_bookmark.repository.repo_name
557 base_redirect_url = h.route_path(
558 base_redirect_url = h.route_path(
558 'repo_summary', repo_name=repo_name)
559 'repo_summary', repo_name=repo_name)
559 if user_bookmark.redirect_url and \
560 if user_bookmark.redirect_url and \
560 '${repo_url}' in user_bookmark.redirect_url:
561 '${repo_url}' in user_bookmark.redirect_url:
561 redirect_url = string.Template(user_bookmark.redirect_url)\
562 redirect_url = string.Template(user_bookmark.redirect_url)\
562 .safe_substitute({'repo_url': base_redirect_url})
563 .safe_substitute({'repo_url': base_redirect_url})
563 else:
564 else:
564 redirect_url = base_redirect_url
565 redirect_url = base_redirect_url
565 # repository group set
566 # repository group set
566 elif user_bookmark.repository_group:
567 elif user_bookmark.repository_group:
567 repo_group_name = user_bookmark.repository_group.group_name
568 repo_group_name = user_bookmark.repository_group.group_name
568 base_redirect_url = h.route_path(
569 base_redirect_url = h.route_path(
569 'repo_group_home', repo_group_name=repo_group_name)
570 'repo_group_home', repo_group_name=repo_group_name)
570 if user_bookmark.redirect_url and \
571 if user_bookmark.redirect_url and \
571 '${repo_group_url}' in user_bookmark.redirect_url:
572 '${repo_group_url}' in user_bookmark.redirect_url:
572 redirect_url = string.Template(user_bookmark.redirect_url)\
573 redirect_url = string.Template(user_bookmark.redirect_url)\
573 .safe_substitute({'repo_group_url': base_redirect_url})
574 .safe_substitute({'repo_group_url': base_redirect_url})
574 else:
575 else:
575 redirect_url = base_redirect_url
576 redirect_url = base_redirect_url
576 # custom URL set
577 # custom URL set
577 elif user_bookmark.redirect_url:
578 elif user_bookmark.redirect_url:
578 server_url = h.route_url('home').rstrip('/')
579 server_url = h.route_url('home').rstrip('/')
579 redirect_url = string.Template(user_bookmark.redirect_url) \
580 redirect_url = string.Template(user_bookmark.redirect_url) \
580 .safe_substitute({'server_url': server_url})
581 .safe_substitute({'server_url': server_url})
581
582
582 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
583 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
583 raise HTTPFound(redirect_url)
584 raise HTTPFound(redirect_url)
584
585
585 @LoginRequired()
586 @LoginRequired()
586 @NotAnonymous()
587 @NotAnonymous()
587 @view_config(
588 @view_config(
588 route_name='my_account_perms', request_method='GET',
589 route_name='my_account_perms', request_method='GET',
589 renderer='rhodecode:templates/admin/my_account/my_account.mako')
590 renderer='rhodecode:templates/admin/my_account/my_account.mako')
590 def my_account_perms(self):
591 def my_account_perms(self):
591 c = self.load_default_context()
592 c = self.load_default_context()
592 c.active = 'perms'
593 c.active = 'perms'
593
594
594 c.perm_user = c.auth_user
595 c.perm_user = c.auth_user
595 return self._get_template_context(c)
596 return self._get_template_context(c)
596
597
597 @LoginRequired()
598 @LoginRequired()
598 @NotAnonymous()
599 @NotAnonymous()
599 @view_config(
600 @view_config(
600 route_name='my_account_notifications', request_method='GET',
601 route_name='my_account_notifications', request_method='GET',
601 renderer='rhodecode:templates/admin/my_account/my_account.mako')
602 renderer='rhodecode:templates/admin/my_account/my_account.mako')
602 def my_notifications(self):
603 def my_notifications(self):
603 c = self.load_default_context()
604 c = self.load_default_context()
604 c.active = 'notifications'
605 c.active = 'notifications'
605
606
606 return self._get_template_context(c)
607 return self._get_template_context(c)
607
608
608 @LoginRequired()
609 @LoginRequired()
609 @NotAnonymous()
610 @NotAnonymous()
610 @CSRFRequired()
611 @CSRFRequired()
611 @view_config(
612 @view_config(
612 route_name='my_account_notifications_toggle_visibility',
613 route_name='my_account_notifications_toggle_visibility',
613 request_method='POST', renderer='json_ext')
614 request_method='POST', renderer='json_ext')
614 def my_notifications_toggle_visibility(self):
615 def my_notifications_toggle_visibility(self):
615 user = self._rhodecode_db_user
616 user = self._rhodecode_db_user
616 new_status = not user.user_data.get('notification_status', True)
617 new_status = not user.user_data.get('notification_status', True)
617 user.update_userdata(notification_status=new_status)
618 user.update_userdata(notification_status=new_status)
618 Session().commit()
619 Session().commit()
619 return user.user_data['notification_status']
620 return user.user_data['notification_status']
620
621
621 @LoginRequired()
622 @LoginRequired()
622 @NotAnonymous()
623 @NotAnonymous()
623 @view_config(
624 @view_config(
624 route_name='my_account_edit',
625 route_name='my_account_edit',
625 request_method='GET',
626 request_method='GET',
626 renderer='rhodecode:templates/admin/my_account/my_account.mako')
627 renderer='rhodecode:templates/admin/my_account/my_account.mako')
627 def my_account_edit(self):
628 def my_account_edit(self):
628 c = self.load_default_context()
629 c = self.load_default_context()
629 c.active = 'profile_edit'
630 c.active = 'profile_edit'
630 c.extern_type = c.user.extern_type
631 c.extern_type = c.user.extern_type
631 c.extern_name = c.user.extern_name
632 c.extern_name = c.user.extern_name
632
633
633 schema = user_schema.UserProfileSchema().bind(
634 schema = user_schema.UserProfileSchema().bind(
634 username=c.user.username, user_emails=c.user.emails)
635 username=c.user.username, user_emails=c.user.emails)
635 appstruct = {
636 appstruct = {
636 'username': c.user.username,
637 'username': c.user.username,
637 'email': c.user.email,
638 'email': c.user.email,
638 'firstname': c.user.firstname,
639 'firstname': c.user.firstname,
639 'lastname': c.user.lastname,
640 'lastname': c.user.lastname,
640 'description': c.user.description,
641 'description': c.user.description,
641 }
642 }
642 c.form = forms.RcForm(
643 c.form = forms.RcForm(
643 schema, appstruct=appstruct,
644 schema, appstruct=appstruct,
644 action=h.route_path('my_account_update'),
645 action=h.route_path('my_account_update'),
645 buttons=(forms.buttons.save, forms.buttons.reset))
646 buttons=(forms.buttons.save, forms.buttons.reset))
646
647
647 return self._get_template_context(c)
648 return self._get_template_context(c)
648
649
649 @LoginRequired()
650 @LoginRequired()
650 @NotAnonymous()
651 @NotAnonymous()
651 @CSRFRequired()
652 @CSRFRequired()
652 @view_config(
653 @view_config(
653 route_name='my_account_update',
654 route_name='my_account_update',
654 request_method='POST',
655 request_method='POST',
655 renderer='rhodecode:templates/admin/my_account/my_account.mako')
656 renderer='rhodecode:templates/admin/my_account/my_account.mako')
656 def my_account_update(self):
657 def my_account_update(self):
657 _ = self.request.translate
658 _ = self.request.translate
658 c = self.load_default_context()
659 c = self.load_default_context()
659 c.active = 'profile_edit'
660 c.active = 'profile_edit'
660 c.perm_user = c.auth_user
661 c.perm_user = c.auth_user
661 c.extern_type = c.user.extern_type
662 c.extern_type = c.user.extern_type
662 c.extern_name = c.user.extern_name
663 c.extern_name = c.user.extern_name
663
664
664 schema = user_schema.UserProfileSchema().bind(
665 schema = user_schema.UserProfileSchema().bind(
665 username=c.user.username, user_emails=c.user.emails)
666 username=c.user.username, user_emails=c.user.emails)
666 form = forms.RcForm(
667 form = forms.RcForm(
667 schema, buttons=(forms.buttons.save, forms.buttons.reset))
668 schema, buttons=(forms.buttons.save, forms.buttons.reset))
668
669
669 controls = self.request.POST.items()
670 controls = self.request.POST.items()
670 try:
671 try:
671 valid_data = form.validate(controls)
672 valid_data = form.validate(controls)
672 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
673 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
673 'new_password', 'password_confirmation']
674 'new_password', 'password_confirmation']
674 if c.extern_type != "rhodecode":
675 if c.extern_type != "rhodecode":
675 # forbid updating username for external accounts
676 # forbid updating username for external accounts
676 skip_attrs.append('username')
677 skip_attrs.append('username')
677 old_email = c.user.email
678 old_email = c.user.email
678 UserModel().update_user(
679 UserModel().update_user(
679 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
680 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
680 **valid_data)
681 **valid_data)
681 if old_email != valid_data['email']:
682 if old_email != valid_data['email']:
682 old = UserEmailMap.query() \
683 old = UserEmailMap.query() \
683 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
684 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
684 old.email = old_email
685 old.email = old_email
685 h.flash(_('Your account was updated successfully'), category='success')
686 h.flash(_('Your account was updated successfully'), category='success')
686 Session().commit()
687 Session().commit()
687 except forms.ValidationFailure as e:
688 except forms.ValidationFailure as e:
688 c.form = e
689 c.form = e
689 return self._get_template_context(c)
690 return self._get_template_context(c)
690 except Exception:
691 except Exception:
691 log.exception("Exception updating user")
692 log.exception("Exception updating user")
692 h.flash(_('Error occurred during update of user'),
693 h.flash(_('Error occurred during update of user'),
693 category='error')
694 category='error')
694 raise HTTPFound(h.route_path('my_account_profile'))
695 raise HTTPFound(h.route_path('my_account_profile'))
695
696
696 def _get_pull_requests_list(self, statuses):
697 def _get_pull_requests_list(self, statuses):
697 draw, start, limit = self._extract_chunk(self.request)
698 draw, start, limit = self._extract_chunk(self.request)
698 search_q, order_by, order_dir = self._extract_ordering(self.request)
699 search_q, order_by, order_dir = self._extract_ordering(self.request)
699 _render = self.request.get_partial_renderer(
700 _render = self.request.get_partial_renderer(
700 'rhodecode:templates/data_table/_dt_elements.mako')
701 'rhodecode:templates/data_table/_dt_elements.mako')
701
702
702 pull_requests = PullRequestModel().get_im_participating_in(
703 pull_requests = PullRequestModel().get_im_participating_in(
703 user_id=self._rhodecode_user.user_id,
704 user_id=self._rhodecode_user.user_id,
704 statuses=statuses,
705 statuses=statuses,
705 offset=start, length=limit, order_by=order_by,
706 offset=start, length=limit, order_by=order_by,
706 order_dir=order_dir)
707 order_dir=order_dir)
707
708
708 pull_requests_total_count = PullRequestModel().count_im_participating_in(
709 pull_requests_total_count = PullRequestModel().count_im_participating_in(
709 user_id=self._rhodecode_user.user_id, statuses=statuses)
710 user_id=self._rhodecode_user.user_id, statuses=statuses)
710
711
711 data = []
712 data = []
712 comments_model = CommentsModel()
713 comments_model = CommentsModel()
713 for pr in pull_requests:
714 for pr in pull_requests:
714 repo_id = pr.target_repo_id
715 repo_id = pr.target_repo_id
715 comments = comments_model.get_all_comments(
716 comments = comments_model.get_all_comments(
716 repo_id, pull_request=pr)
717 repo_id, pull_request=pr)
717 owned = pr.user_id == self._rhodecode_user.user_id
718 owned = pr.user_id == self._rhodecode_user.user_id
718
719
719 data.append({
720 data.append({
720 'target_repo': _render('pullrequest_target_repo',
721 'target_repo': _render('pullrequest_target_repo',
721 pr.target_repo.repo_name),
722 pr.target_repo.repo_name),
722 'name': _render('pullrequest_name',
723 'name': _render('pullrequest_name',
723 pr.pull_request_id, pr.pull_request_state,
724 pr.pull_request_id, pr.pull_request_state,
724 pr.work_in_progress, pr.target_repo.repo_name,
725 pr.work_in_progress, pr.target_repo.repo_name,
725 short=True),
726 short=True),
726 'name_raw': pr.pull_request_id,
727 'name_raw': pr.pull_request_id,
727 'status': _render('pullrequest_status',
728 'status': _render('pullrequest_status',
728 pr.calculated_review_status()),
729 pr.calculated_review_status()),
729 'title': _render('pullrequest_title', pr.title, pr.description),
730 'title': _render('pullrequest_title', pr.title, pr.description),
730 'description': h.escape(pr.description),
731 'description': h.escape(pr.description),
731 'updated_on': _render('pullrequest_updated_on',
732 'updated_on': _render('pullrequest_updated_on',
732 h.datetime_to_time(pr.updated_on)),
733 h.datetime_to_time(pr.updated_on)),
733 'updated_on_raw': h.datetime_to_time(pr.updated_on),
734 'updated_on_raw': h.datetime_to_time(pr.updated_on),
734 'created_on': _render('pullrequest_updated_on',
735 'created_on': _render('pullrequest_updated_on',
735 h.datetime_to_time(pr.created_on)),
736 h.datetime_to_time(pr.created_on)),
736 'created_on_raw': h.datetime_to_time(pr.created_on),
737 'created_on_raw': h.datetime_to_time(pr.created_on),
737 'state': pr.pull_request_state,
738 'state': pr.pull_request_state,
738 'author': _render('pullrequest_author',
739 'author': _render('pullrequest_author',
739 pr.author.full_contact, ),
740 pr.author.full_contact, ),
740 'author_raw': pr.author.full_name,
741 'author_raw': pr.author.full_name,
741 'comments': _render('pullrequest_comments', len(comments)),
742 'comments': _render('pullrequest_comments', len(comments)),
742 'comments_raw': len(comments),
743 'comments_raw': len(comments),
743 'closed': pr.is_closed(),
744 'closed': pr.is_closed(),
744 'owned': owned
745 'owned': owned
745 })
746 })
746
747
747 # json used to render the grid
748 # json used to render the grid
748 data = ({
749 data = ({
749 'draw': draw,
750 'draw': draw,
750 'data': data,
751 'data': data,
751 'recordsTotal': pull_requests_total_count,
752 'recordsTotal': pull_requests_total_count,
752 'recordsFiltered': pull_requests_total_count,
753 'recordsFiltered': pull_requests_total_count,
753 })
754 })
754 return data
755 return data
755
756
756 @LoginRequired()
757 @LoginRequired()
757 @NotAnonymous()
758 @NotAnonymous()
758 @view_config(
759 @view_config(
759 route_name='my_account_pullrequests',
760 route_name='my_account_pullrequests',
760 request_method='GET',
761 request_method='GET',
761 renderer='rhodecode:templates/admin/my_account/my_account.mako')
762 renderer='rhodecode:templates/admin/my_account/my_account.mako')
762 def my_account_pullrequests(self):
763 def my_account_pullrequests(self):
763 c = self.load_default_context()
764 c = self.load_default_context()
764 c.active = 'pullrequests'
765 c.active = 'pullrequests'
765 req_get = self.request.GET
766 req_get = self.request.GET
766
767
767 c.closed = str2bool(req_get.get('pr_show_closed'))
768 c.closed = str2bool(req_get.get('pr_show_closed'))
768
769
769 return self._get_template_context(c)
770 return self._get_template_context(c)
770
771
771 @LoginRequired()
772 @LoginRequired()
772 @NotAnonymous()
773 @NotAnonymous()
773 @view_config(
774 @view_config(
774 route_name='my_account_pullrequests_data',
775 route_name='my_account_pullrequests_data',
775 request_method='GET', renderer='json_ext')
776 request_method='GET', renderer='json_ext')
776 def my_account_pullrequests_data(self):
777 def my_account_pullrequests_data(self):
777 self.load_default_context()
778 self.load_default_context()
778 req_get = self.request.GET
779 req_get = self.request.GET
779 closed = str2bool(req_get.get('closed'))
780 closed = str2bool(req_get.get('closed'))
780
781
781 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
782 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
782 if closed:
783 if closed:
783 statuses += [PullRequest.STATUS_CLOSED]
784 statuses += [PullRequest.STATUS_CLOSED]
784
785
785 data = self._get_pull_requests_list(statuses=statuses)
786 data = self._get_pull_requests_list(statuses=statuses)
786 return data
787 return data
787
788
788 @LoginRequired()
789 @LoginRequired()
789 @NotAnonymous()
790 @NotAnonymous()
790 @view_config(
791 @view_config(
791 route_name='my_account_user_group_membership',
792 route_name='my_account_user_group_membership',
792 request_method='GET',
793 request_method='GET',
793 renderer='rhodecode:templates/admin/my_account/my_account.mako')
794 renderer='rhodecode:templates/admin/my_account/my_account.mako')
794 def my_account_user_group_membership(self):
795 def my_account_user_group_membership(self):
795 c = self.load_default_context()
796 c = self.load_default_context()
796 c.active = 'user_group_membership'
797 c.active = 'user_group_membership'
797 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
798 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
798 for group in self._rhodecode_db_user.group_member]
799 for group in self._rhodecode_db_user.group_member]
799 c.user_groups = json.dumps(groups)
800 c.user_groups = json.dumps(groups)
800 return self._get_template_context(c)
801 return self._get_template_context(c)
@@ -1,156 +1,159 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
24 from pyramid.view import view_config
25
25
26 from rhodecode.apps._base import BaseAppView, DataGridAppView
26 from rhodecode.apps._base import BaseAppView, DataGridAppView
27 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
27 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
28 from rhodecode.events import trigger
28 from rhodecode.events import trigger
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
31 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
32 from rhodecode.model.db import IntegrityError, UserSshKeys
32 from rhodecode.model.db import IntegrityError, UserSshKeys
33 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
34 from rhodecode.model.ssh_key import SshKeyModel
34 from rhodecode.model.ssh_key import SshKeyModel
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
39 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
40
40
41 def load_default_context(self):
41 def load_default_context(self):
42 c = self._get_local_tmpl_context()
42 c = self._get_local_tmpl_context()
43 c.user = c.auth_user.get_instance()
43 c.user = c.auth_user.get_instance()
44
44
45 c.ssh_enabled = self.request.registry.settings.get(
45 c.ssh_enabled = self.request.registry.settings.get(
46 'ssh.generate_authorized_keyfile')
46 'ssh.generate_authorized_keyfile')
47
47
48 return c
48 return c
49
49
50 @LoginRequired()
50 @LoginRequired()
51 @NotAnonymous()
51 @NotAnonymous()
52 @view_config(
52 @view_config(
53 route_name='my_account_ssh_keys', request_method='GET',
53 route_name='my_account_ssh_keys', request_method='GET',
54 renderer='rhodecode:templates/admin/my_account/my_account.mako')
54 renderer='rhodecode:templates/admin/my_account/my_account.mako')
55 def my_account_ssh_keys(self):
55 def my_account_ssh_keys(self):
56 _ = self.request.translate
56 _ = self.request.translate
57
57
58 c = self.load_default_context()
58 c = self.load_default_context()
59 c.active = 'ssh_keys'
59 c.active = 'ssh_keys'
60 c.default_key = self.request.GET.get('default_key')
60 c.default_key = self.request.GET.get('default_key')
61 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
61 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
62 return self._get_template_context(c)
62 return self._get_template_context(c)
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @NotAnonymous()
65 @NotAnonymous()
66 @view_config(
66 @view_config(
67 route_name='my_account_ssh_keys_generate', request_method='GET',
67 route_name='my_account_ssh_keys_generate', request_method='GET',
68 renderer='rhodecode:templates/admin/my_account/my_account.mako')
68 renderer='rhodecode:templates/admin/my_account/my_account.mako')
69 def ssh_keys_generate_keypair(self):
69 def ssh_keys_generate_keypair(self):
70 _ = self.request.translate
70 _ = self.request.translate
71 c = self.load_default_context()
71 c = self.load_default_context()
72
72
73 c.active = 'ssh_keys_generate'
73 c.active = 'ssh_keys_generate'
74 if c.ssh_key_generator_enabled:
74 if c.ssh_key_generator_enabled:
75 private_format = self.request.GET.get('private_format') \
76 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
75 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
77 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
76 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
78 c.private, c.public = SshKeyModel().generate_keypair(
79 comment=comment, private_format=private_format)
77 c.target_form_url = h.route_path(
80 c.target_form_url = h.route_path(
78 'my_account_ssh_keys', _query=dict(default_key=c.public))
81 'my_account_ssh_keys', _query=dict(default_key=c.public))
79 return self._get_template_context(c)
82 return self._get_template_context(c)
80
83
81 @LoginRequired()
84 @LoginRequired()
82 @NotAnonymous()
85 @NotAnonymous()
83 @CSRFRequired()
86 @CSRFRequired()
84 @view_config(
87 @view_config(
85 route_name='my_account_ssh_keys_add', request_method='POST',)
88 route_name='my_account_ssh_keys_add', request_method='POST',)
86 def my_account_ssh_keys_add(self):
89 def my_account_ssh_keys_add(self):
87 _ = self.request.translate
90 _ = self.request.translate
88 c = self.load_default_context()
91 c = self.load_default_context()
89
92
90 user_data = c.user.get_api_data()
93 user_data = c.user.get_api_data()
91 key_data = self.request.POST.get('key_data')
94 key_data = self.request.POST.get('key_data')
92 description = self.request.POST.get('description')
95 description = self.request.POST.get('description')
93 fingerprint = 'unknown'
96 fingerprint = 'unknown'
94 try:
97 try:
95 if not key_data:
98 if not key_data:
96 raise ValueError('Please add a valid public key')
99 raise ValueError('Please add a valid public key')
97
100
98 key = SshKeyModel().parse_key(key_data.strip())
101 key = SshKeyModel().parse_key(key_data.strip())
99 fingerprint = key.hash_md5()
102 fingerprint = key.hash_md5()
100
103
101 ssh_key = SshKeyModel().create(
104 ssh_key = SshKeyModel().create(
102 c.user.user_id, fingerprint, key.keydata, description)
105 c.user.user_id, fingerprint, key.keydata, description)
103 ssh_key_data = ssh_key.get_api_data()
106 ssh_key_data = ssh_key.get_api_data()
104
107
105 audit_logger.store_web(
108 audit_logger.store_web(
106 'user.edit.ssh_key.add', action_data={
109 'user.edit.ssh_key.add', action_data={
107 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
110 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
108 user=self._rhodecode_user, )
111 user=self._rhodecode_user, )
109 Session().commit()
112 Session().commit()
110
113
111 # Trigger an event on change of keys.
114 # Trigger an event on change of keys.
112 trigger(SshKeyFileChangeEvent(), self.request.registry)
115 trigger(SshKeyFileChangeEvent(), self.request.registry)
113
116
114 h.flash(_("Ssh Key successfully created"), category='success')
117 h.flash(_("Ssh Key successfully created"), category='success')
115
118
116 except IntegrityError:
119 except IntegrityError:
117 log.exception("Exception during ssh key saving")
120 log.exception("Exception during ssh key saving")
118 err = 'Such key with fingerprint `{}` already exists, ' \
121 err = 'Such key with fingerprint `{}` already exists, ' \
119 'please use a different one'.format(fingerprint)
122 'please use a different one'.format(fingerprint)
120 h.flash(_('An error occurred during ssh key saving: {}').format(err),
123 h.flash(_('An error occurred during ssh key saving: {}').format(err),
121 category='error')
124 category='error')
122 except Exception as e:
125 except Exception as e:
123 log.exception("Exception during ssh key saving")
126 log.exception("Exception during ssh key saving")
124 h.flash(_('An error occurred during ssh key saving: {}').format(e),
127 h.flash(_('An error occurred during ssh key saving: {}').format(e),
125 category='error')
128 category='error')
126
129
127 return HTTPFound(h.route_path('my_account_ssh_keys'))
130 return HTTPFound(h.route_path('my_account_ssh_keys'))
128
131
129 @LoginRequired()
132 @LoginRequired()
130 @NotAnonymous()
133 @NotAnonymous()
131 @CSRFRequired()
134 @CSRFRequired()
132 @view_config(
135 @view_config(
133 route_name='my_account_ssh_keys_delete', request_method='POST')
136 route_name='my_account_ssh_keys_delete', request_method='POST')
134 def my_account_ssh_keys_delete(self):
137 def my_account_ssh_keys_delete(self):
135 _ = self.request.translate
138 _ = self.request.translate
136 c = self.load_default_context()
139 c = self.load_default_context()
137
140
138 user_data = c.user.get_api_data()
141 user_data = c.user.get_api_data()
139
142
140 del_ssh_key = self.request.POST.get('del_ssh_key')
143 del_ssh_key = self.request.POST.get('del_ssh_key')
141
144
142 if del_ssh_key:
145 if del_ssh_key:
143 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
146 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
144 ssh_key_data = ssh_key.get_api_data()
147 ssh_key_data = ssh_key.get_api_data()
145
148
146 SshKeyModel().delete(del_ssh_key, c.user.user_id)
149 SshKeyModel().delete(del_ssh_key, c.user.user_id)
147 audit_logger.store_web(
150 audit_logger.store_web(
148 'user.edit.ssh_key.delete', action_data={
151 'user.edit.ssh_key.delete', action_data={
149 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
152 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
150 user=self._rhodecode_user,)
153 user=self._rhodecode_user,)
151 Session().commit()
154 Session().commit()
152 # Trigger an event on change of keys.
155 # Trigger an event on change of keys.
153 trigger(SshKeyFileChangeEvent(), self.request.registry)
156 trigger(SshKeyFileChangeEvent(), self.request.registry)
154 h.flash(_("Ssh key successfully deleted"), category='success')
157 h.flash(_("Ssh key successfully deleted"), category='success')
155
158
156 return HTTPFound(h.route_path('my_account_ssh_keys'))
159 return HTTPFound(h.route_path('my_account_ssh_keys'))
@@ -1,103 +1,110 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.apps._base import RepoGroupAppView
26 from rhodecode.apps._base import RepoGroupAppView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
28 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
30 LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.model.db import User
31 from rhodecode.model.permission import PermissionModel
32 from rhodecode.model.permission import PermissionModel
32 from rhodecode.model.repo_group import RepoGroupModel
33 from rhodecode.model.repo_group import RepoGroupModel
33 from rhodecode.model.forms import RepoGroupPermsForm
34 from rhodecode.model.forms import RepoGroupPermsForm
34 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
35
36
36 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
37
38
38
39
39 class RepoGroupPermissionsView(RepoGroupAppView):
40 class RepoGroupPermissionsView(RepoGroupAppView):
40 def load_default_context(self):
41 def load_default_context(self):
41 c = self._get_local_tmpl_context()
42 c = self._get_local_tmpl_context()
42
43
43 return c
44 return c
44
45
45 @LoginRequired()
46 @LoginRequired()
46 @HasRepoGroupPermissionAnyDecorator('group.admin')
47 @HasRepoGroupPermissionAnyDecorator('group.admin')
47 @view_config(
48 @view_config(
48 route_name='edit_repo_group_perms', request_method='GET',
49 route_name='edit_repo_group_perms', request_method='GET',
49 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
50 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
50 def edit_repo_group_permissions(self):
51 def edit_repo_group_permissions(self):
51 c = self.load_default_context()
52 c = self.load_default_context()
52 c.active = 'permissions'
53 c.active = 'permissions'
53 c.repo_group = self.db_repo_group
54 c.repo_group = self.db_repo_group
54 return self._get_template_context(c)
55 return self._get_template_context(c)
55
56
56 @LoginRequired()
57 @LoginRequired()
57 @HasRepoGroupPermissionAnyDecorator('group.admin')
58 @HasRepoGroupPermissionAnyDecorator('group.admin')
58 @CSRFRequired()
59 @CSRFRequired()
59 @view_config(
60 @view_config(
60 route_name='edit_repo_group_perms_update', request_method='POST',
61 route_name='edit_repo_group_perms_update', request_method='POST',
61 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
62 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
62 def edit_repo_groups_permissions_update(self):
63 def edit_repo_groups_permissions_update(self):
63 _ = self.request.translate
64 _ = self.request.translate
64 c = self.load_default_context()
65 c = self.load_default_context()
65 c.active = 'perms'
66 c.active = 'perms'
66 c.repo_group = self.db_repo_group
67 c.repo_group = self.db_repo_group
67
68
68 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
69 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
69 form = RepoGroupPermsForm(self.request.translate, valid_recursive_choices)()\
70 form = RepoGroupPermsForm(self.request.translate, valid_recursive_choices)()\
70 .to_python(self.request.POST)
71 .to_python(self.request.POST)
71
72
72 if not c.rhodecode_user.is_admin:
73 if not c.rhodecode_user.is_admin:
73 if self._revoke_perms_on_yourself(form):
74 if self._revoke_perms_on_yourself(form):
74 msg = _('Cannot change permission for yourself as admin')
75 msg = _('Cannot change permission for yourself as admin')
75 h.flash(msg, category='warning')
76 h.flash(msg, category='warning')
76 raise HTTPFound(
77 raise HTTPFound(
77 h.route_path('edit_repo_group_perms',
78 h.route_path('edit_repo_group_perms',
78 repo_group_name=self.db_repo_group_name))
79 repo_group_name=self.db_repo_group_name))
79
80
80 # iterate over all members(if in recursive mode) of this groups and
81 # iterate over all members(if in recursive mode) of this groups and
81 # set the permissions !
82 # set the permissions !
82 # this can be potentially heavy operation
83 # this can be potentially heavy operation
83 changes = RepoGroupModel().update_permissions(
84 changes = RepoGroupModel().update_permissions(
84 c.repo_group,
85 c.repo_group,
85 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
86 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
86 form['recursive'])
87 form['recursive'])
87
88
88 action_data = {
89 action_data = {
89 'added': changes['added'],
90 'added': changes['added'],
90 'updated': changes['updated'],
91 'updated': changes['updated'],
91 'deleted': changes['deleted'],
92 'deleted': changes['deleted'],
92 }
93 }
93 audit_logger.store_web(
94 audit_logger.store_web(
94 'repo_group.edit.permissions', action_data=action_data,
95 'repo_group.edit.permissions', action_data=action_data,
95 user=c.rhodecode_user)
96 user=c.rhodecode_user)
96
97
97 Session().commit()
98 Session().commit()
98 h.flash(_('Repository Group permissions updated'), category='success')
99 h.flash(_('Repository Group permissions updated'), category='success')
99 PermissionModel().flush_user_permission_caches(changes)
100
101 affected_user_ids = None
102 if changes.get('default_user_changed', False):
103 # if we change the default user, we need to flush everyone permissions
104 affected_user_ids = User.get_all_user_ids()
105 PermissionModel().flush_user_permission_caches(
106 changes, affected_user_ids=affected_user_ids)
100
107
101 raise HTTPFound(
108 raise HTTPFound(
102 h.route_path('edit_repo_group_perms',
109 h.route_path('edit_repo_group_perms',
103 repo_group_name=self.db_repo_group_name))
110 repo_group_name=self.db_repo_group_name))
@@ -1,122 +1,135 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
24 from pyramid.view import view_config
25
25
26 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
28 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.db import User
31 from rhodecode.model.forms import RepoPermsForm
33 from rhodecode.model.forms import RepoPermsForm
32 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
33 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.permission import PermissionModel
34 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.repo import RepoModel
35
37
36 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
37
39
38
40
39 class RepoSettingsPermissionsView(RepoAppView):
41 class RepoSettingsPermissionsView(RepoAppView):
40
42
41 def load_default_context(self):
43 def load_default_context(self):
42 c = self._get_local_tmpl_context()
44 c = self._get_local_tmpl_context()
43 return c
45 return c
44
46
45 @LoginRequired()
47 @LoginRequired()
46 @HasRepoPermissionAnyDecorator('repository.admin')
48 @HasRepoPermissionAnyDecorator('repository.admin')
47 @view_config(
49 @view_config(
48 route_name='edit_repo_perms', request_method='GET',
50 route_name='edit_repo_perms', request_method='GET',
49 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
51 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
50 def edit_permissions(self):
52 def edit_permissions(self):
51 _ = self.request.translate
53 _ = self.request.translate
52 c = self.load_default_context()
54 c = self.load_default_context()
53 c.active = 'permissions'
55 c.active = 'permissions'
54 if self.request.GET.get('branch_permissions'):
56 if self.request.GET.get('branch_permissions'):
55 h.flash(_('Explicitly add user or user group with write+ '
57 h.flash(_('Explicitly add user or user group with write+ '
56 'permission to modify their branch permissions.'),
58 'permission to modify their branch permissions.'),
57 category='notice')
59 category='notice')
58 return self._get_template_context(c)
60 return self._get_template_context(c)
59
61
60 @LoginRequired()
62 @LoginRequired()
61 @HasRepoPermissionAnyDecorator('repository.admin')
63 @HasRepoPermissionAnyDecorator('repository.admin')
62 @CSRFRequired()
64 @CSRFRequired()
63 @view_config(
65 @view_config(
64 route_name='edit_repo_perms', request_method='POST',
66 route_name='edit_repo_perms', request_method='POST',
65 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
67 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
66 def edit_permissions_update(self):
68 def edit_permissions_update(self):
67 _ = self.request.translate
69 _ = self.request.translate
68 c = self.load_default_context()
70 c = self.load_default_context()
69 c.active = 'permissions'
71 c.active = 'permissions'
70 data = self.request.POST
72 data = self.request.POST
71 # store private flag outside of HTML to verify if we can modify
73 # store private flag outside of HTML to verify if we can modify
72 # default user permissions, prevents submission of FAKE post data
74 # default user permissions, prevents submission of FAKE post data
73 # into the form for private repos
75 # into the form for private repos
74 data['repo_private'] = self.db_repo.private
76 data['repo_private'] = self.db_repo.private
75 form = RepoPermsForm(self.request.translate)().to_python(data)
77 form = RepoPermsForm(self.request.translate)().to_python(data)
76 changes = RepoModel().update_permissions(
78 changes = RepoModel().update_permissions(
77 self.db_repo_name, form['perm_additions'], form['perm_updates'],
79 self.db_repo_name, form['perm_additions'], form['perm_updates'],
78 form['perm_deletions'])
80 form['perm_deletions'])
79
81
80 action_data = {
82 action_data = {
81 'added': changes['added'],
83 'added': changes['added'],
82 'updated': changes['updated'],
84 'updated': changes['updated'],
83 'deleted': changes['deleted'],
85 'deleted': changes['deleted'],
84 }
86 }
85 audit_logger.store_web(
87 audit_logger.store_web(
86 'repo.edit.permissions', action_data=action_data,
88 'repo.edit.permissions', action_data=action_data,
87 user=self._rhodecode_user, repo=self.db_repo)
89 user=self._rhodecode_user, repo=self.db_repo)
88
90
89 Session().commit()
91 Session().commit()
90 h.flash(_('Repository access permissions updated'), category='success')
92 h.flash(_('Repository access permissions updated'), category='success')
91
93
92 PermissionModel().flush_user_permission_caches(changes)
94 affected_user_ids = None
95 if changes.get('default_user_changed', False):
96 # if we change the default user, we need to flush everyone permissions
97 affected_user_ids = User.get_all_user_ids()
98 PermissionModel().flush_user_permission_caches(
99 changes, affected_user_ids=affected_user_ids)
93
100
94 raise HTTPFound(
101 raise HTTPFound(
95 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
102 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
96
103
97 @LoginRequired()
104 @LoginRequired()
98 @HasRepoPermissionAnyDecorator('repository.admin')
105 @HasRepoPermissionAnyDecorator('repository.admin')
99 @CSRFRequired()
106 @CSRFRequired()
100 @view_config(
107 @view_config(
101 route_name='edit_repo_perms_set_private', request_method='POST',
108 route_name='edit_repo_perms_set_private', request_method='POST',
102 renderer='json_ext')
109 renderer='json_ext')
103 def edit_permissions_set_private_repo(self):
110 def edit_permissions_set_private_repo(self):
104 _ = self.request.translate
111 _ = self.request.translate
105 self.load_default_context()
112 self.load_default_context()
106
113
114 private_flag = str2bool(self.request.POST.get('private'))
115
107 try:
116 try:
108 RepoModel().update(
117 RepoModel().update(
109 self.db_repo, **{'repo_private': True, 'repo_name': self.db_repo_name})
118 self.db_repo, **{'repo_private': private_flag, 'repo_name': self.db_repo_name})
110 Session().commit()
119 Session().commit()
111
120
112 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
121 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
113 category='success')
122 category='success')
114 except Exception:
123 except Exception:
115 log.exception("Exception during update of repository")
124 log.exception("Exception during update of repository")
116 h.flash(_('Error occurred during update of repository {}').format(
125 h.flash(_('Error occurred during update of repository {}').format(
117 self.db_repo_name), category='error')
126 self.db_repo_name), category='error')
118
127
128 # NOTE(dan): we change repo private mode we need to notify all USERS
129 affected_user_ids = User.get_all_user_ids()
130 PermissionModel().trigger_permission_flush(affected_user_ids)
131
119 return {
132 return {
120 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
133 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
121 'private': True
134 'private': private_flag
122 }
135 }
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now