##// END OF EJS Templates
#344 optional firstname lastname on user creation...
marcink -
r1950:4ae17f81 beta
parent child Browse files
Show More
@@ -1,473 +1,473 b''
1 1 .. _api:
2 2
3 3
4 4 API
5 5 ===
6 6
7 7
8 8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 9 There's a single schema for calling all api methods. API is implemented
10 10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 11 <your_server>/_admin/api
12 12
13 13 API ACCESS FOR WEB VIEWS
14 14 ++++++++++++++++++++++++
15 15
16 16 API access can also be turned on for each web view in RhodeCode that is
17 17 decorated with `@LoginRequired` decorator. To enable API access simple change
18 18 the standard login decorator to `@LoginRequired(api_access=True)`.
19 19 After this change, a rhodecode view can be accessed without login by adding a
20 20 GET parameter `?api_key=<api_key>` to url. By default this is only
21 21 enabled on RSS/ATOM feed views.
22 22
23 23
24 24 API ACCESS
25 25 ++++++++++
26 26
27 27 All clients are required to send JSON-RPC spec JSON data::
28 28
29 29 {
30 30 "id:<id>,
31 31 "api_key":"<api_key>",
32 32 "method":"<method_name>",
33 33 "args":{"<arg_key>":"<arg_val>"}
34 34 }
35 35
36 36 Example call for autopulling remotes repos using curl::
37 37 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
38 38
39 39 Simply provide
40 40 - *id* A value of any type, which is used to match the response with the request that it is replying to.
41 41 - *api_key* for access and permission validation.
42 42 - *method* is name of method to call
43 43 - *args* is an key:value list of arguments to pass to method
44 44
45 45 .. note::
46 46
47 47 api_key can be found in your user account page
48 48
49 49
50 50 RhodeCode API will return always a JSON-RPC response::
51 51
52 52 {
53 53 "id":<id>,
54 54 "result": "<result>",
55 55 "error": null
56 56 }
57 57
58 58 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
59 59 calling api *error* key from response will contain failure description
60 60 and result will be null.
61 61
62 62 API METHODS
63 63 +++++++++++
64 64
65 65
66 66 pull
67 67 ----
68 68
69 69 Pulls given repo from remote location. Can be used to automatically keep
70 70 remote repos up to date. This command can be executed only using api_key
71 71 belonging to user with admin rights
72 72
73 73 INPUT::
74 74
75 75 api_key : "<api_key>"
76 76 method : "pull"
77 77 args : {
78 78 "repo_name" : "<reponame>"
79 79 }
80 80
81 81 OUTPUT::
82 82
83 83 result : "Pulled from <reponame>"
84 84 error : null
85 85
86 86
87 87 get_user
88 88 --------
89 89
90 90 Get's an user by username, Returns empty result if user is not found.
91 91 This command can be executed only using api_key belonging to user with admin
92 92 rights.
93 93
94 94 INPUT::
95 95
96 96 api_key : "<api_key>"
97 97 method : "get_user"
98 98 args : {
99 99 "username" : "<username>"
100 100 }
101 101
102 102 OUTPUT::
103 103
104 104 result: None if user does not exist or
105 105 {
106 106 "id" : "<id>",
107 107 "username" : "<username>",
108 108 "firstname": "<firstname>",
109 109 "lastname" : "<lastname>",
110 110 "email" : "<email>",
111 111 "active" : "<bool>",
112 112 "admin" :Β  "<bool>",
113 113 "ldap" : "<ldap_dn>"
114 114 }
115 115
116 116 error: null
117 117
118 118
119 119 get_users
120 120 ---------
121 121
122 122 Lists all existing users. This command can be executed only using api_key
123 123 belonging to user with admin rights.
124 124
125 125 INPUT::
126 126
127 127 api_key : "<api_key>"
128 128 method : "get_users"
129 129 args : { }
130 130
131 131 OUTPUT::
132 132
133 133 result: [
134 134 {
135 135 "id" : "<id>",
136 136 "username" : "<username>",
137 137 "firstname": "<firstname>",
138 138 "lastname" : "<lastname>",
139 139 "email" : "<email>",
140 140 "active" : "<bool>",
141 141 "admin" :Β  "<bool>",
142 142 "ldap" : "<ldap_dn>"
143 143 },
144 144 …
145 145 ]
146 146 error: null
147 147
148 148 create_user
149 149 -----------
150 150
151 151 Creates new user or updates current one if such user exists. This command can
152 152 be executed only using api_key belonging to user with admin rights.
153 153
154 154 INPUT::
155 155
156 156 api_key : "<api_key>"
157 157 method : "create_user"
158 158 args : {
159 159 "username" : "<username>",
160 160 "password" : "<password>",
161 "firstname" : "<firstname>",
162 "lastname" : "<lastname>",
163 "email" : "<useremail>"
161 "email" : "<useremail>",
162 "firstname" : "<firstname> = None",
163 "lastname" : "<lastname> = None",
164 164 "active" : "<bool> = True",
165 165 "admin" : "<bool> = False",
166 166 "ldap_dn" : "<ldap_dn> = None"
167 167 }
168 168
169 169 OUTPUT::
170 170
171 171 result: {
172 172 "id" : "<new_user_id>",
173 173 "msg" : "created new user <username>"
174 174 }
175 175 error: null
176 176
177 177 get_users_group
178 178 ---------------
179 179
180 180 Gets an existing users group. This command can be executed only using api_key
181 181 belonging to user with admin rights.
182 182
183 183 INPUT::
184 184
185 185 api_key : "<api_key>"
186 186 method : "get_users_group"
187 187 args : {
188 188 "group_name" : "<name>"
189 189 }
190 190
191 191 OUTPUT::
192 192
193 193 result : None if group not exist
194 194 {
195 195 "id" : "<id>",
196 196 "group_name" : "<groupname>",
197 197 "active": "<bool>",
198 198 "members" : [
199 199 { "id" : "<userid>",
200 200 "username" : "<username>",
201 201 "firstname": "<firstname>",
202 202 "lastname" : "<lastname>",
203 203 "email" : "<email>",
204 204 "active" : "<bool>",
205 205 "admin" :Β  "<bool>",
206 206 "ldap" : "<ldap_dn>"
207 207 },
208 208 …
209 209 ]
210 210 }
211 211 error : null
212 212
213 213 get_users_groups
214 214 ----------------
215 215
216 216 Lists all existing users groups. This command can be executed only using
217 217 api_key belonging to user with admin rights.
218 218
219 219 INPUT::
220 220
221 221 api_key : "<api_key>"
222 222 method : "get_users_groups"
223 223 args : { }
224 224
225 225 OUTPUT::
226 226
227 227 result : [
228 228 {
229 229 "id" : "<id>",
230 230 "group_name" : "<groupname>",
231 231 "active": "<bool>",
232 232 "members" : [
233 233 {
234 234 "id" : "<userid>",
235 235 "username" : "<username>",
236 236 "firstname": "<firstname>",
237 237 "lastname" : "<lastname>",
238 238 "email" : "<email>",
239 239 "active" : "<bool>",
240 240 "admin" :Β  "<bool>",
241 241 "ldap" : "<ldap_dn>"
242 242 },
243 243 …
244 244 ]
245 245 }
246 246 ]
247 247 error : null
248 248
249 249
250 250 create_users_group
251 251 ------------------
252 252
253 253 Creates new users group. This command can be executed only using api_key
254 254 belonging to user with admin rights
255 255
256 256 INPUT::
257 257
258 258 api_key : "<api_key>"
259 259 method : "create_users_group"
260 260 args: {
261 261 "group_name": "<groupname>",
262 262 "active":"<bool> = True"
263 263 }
264 264
265 265 OUTPUT::
266 266
267 267 result: {
268 268 "id": "<newusersgroupid>",
269 269 "msg": "created new users group <groupname>"
270 270 }
271 271 error: null
272 272
273 273 add_user_to_users_group
274 274 -----------------------
275 275
276 276 Adds a user to a users group. This command can be executed only using api_key
277 277 belonging to user with admin rights
278 278
279 279 INPUT::
280 280
281 281 api_key : "<api_key>"
282 282 method : "add_user_users_group"
283 283 args: {
284 284 "group_name" : "<groupname>",
285 285 "username" : "<username>"
286 286 }
287 287
288 288 OUTPUT::
289 289
290 290 result: {
291 291 "id": "<newusersgroupmemberid>",
292 292 "msg": "created new users group member"
293 293 }
294 294 error: null
295 295
296 296 get_repo
297 297 --------
298 298
299 299 Gets an existing repository. This command can be executed only using api_key
300 300 belonging to user with admin rights
301 301
302 302 INPUT::
303 303
304 304 api_key : "<api_key>"
305 305 method : "get_repo"
306 306 args: {
307 307 "repo_name" : "<reponame>"
308 308 }
309 309
310 310 OUTPUT::
311 311
312 312 result: None if repository does not exist or
313 313 {
314 314 "id" : "<id>",
315 315 "repo_name" : "<reponame>"
316 316 "type" : "<type>",
317 317 "description" : "<description>",
318 318 "members" : [
319 319 { "id" : "<userid>",
320 320 "username" : "<username>",
321 321 "firstname": "<firstname>",
322 322 "lastname" : "<lastname>",
323 323 "email" : "<email>",
324 324 "active" : "<bool>",
325 325 "admin" :Β  "<bool>",
326 326 "ldap" : "<ldap_dn>",
327 327 "permission" : "repository.(read|write|admin)"
328 328 },
329 329 …
330 330 {
331 331 "id" : "<usersgroupid>",
332 332 "name" : "<usersgroupname>",
333 333 "active": "<bool>",
334 334 "permission" : "repository.(read|write|admin)"
335 335 },
336 336 …
337 337 ]
338 338 }
339 339 error: null
340 340
341 341 get_repos
342 342 ---------
343 343
344 344 Lists all existing repositories. This command can be executed only using api_key
345 345 belonging to user with admin rights
346 346
347 347 INPUT::
348 348
349 349 api_key : "<api_key>"
350 350 method : "get_repos"
351 351 args: { }
352 352
353 353 OUTPUT::
354 354
355 355 result: [
356 356 {
357 357 "id" : "<id>",
358 358 "repo_name" : "<reponame>"
359 359 "type" : "<type>",
360 360 "description" : "<description>"
361 361 },
362 362 …
363 363 ]
364 364 error: null
365 365
366 366
367 367 get_repo_nodes
368 368 --------------
369 369
370 370 returns a list of nodes and it's children in a flat list for a given path
371 371 at given revision. It's possible to specify ret_type to show only `files` or
372 372 `dirs`. This command can be executed only using api_key belonging to user
373 373 with admin rights
374 374
375 375 INPUT::
376 376
377 377 api_key : "<api_key>"
378 378 method : "get_repo_nodes"
379 379 args: {
380 380 "repo_name" : "<reponame>",
381 381 "revision" : "<revision>",
382 382 "root_path" : "<root_path>",
383 383 "ret_type" : "<ret_type>" = 'all'
384 384 }
385 385
386 386 OUTPUT::
387 387
388 388 result: [
389 389 {
390 390 "name" : "<name>"
391 391 "type" : "<type>",
392 392 },
393 393 …
394 394 ]
395 395 error: null
396 396
397 397
398 398
399 399 create_repo
400 400 -----------
401 401
402 402 Creates a repository. This command can be executed only using api_key
403 403 belonging to user with admin rights.
404 404 If repository name contains "/", all needed repository groups will be created.
405 405 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
406 406 and create "baz" repository with "bar" as group.
407 407
408 408 INPUT::
409 409
410 410 api_key : "<api_key>"
411 411 method : "create_repo"
412 412 args: {
413 413 "repo_name" : "<reponame>",
414 414 "owner_name" : "<ownername>",
415 415 "description" : "<description> = ''",
416 416 "repo_type" : "<type> = 'hg'",
417 417 "private" : "<bool> = False"
418 418 }
419 419
420 420 OUTPUT::
421 421
422 422 result: {
423 423 "id": "<newrepoid>",
424 424 "msg": "Created new repository <reponame>",
425 425 }
426 426 error: null
427 427
428 428 add_user_to_repo
429 429 ----------------
430 430
431 431 Add a user to a repository. This command can be executed only using api_key
432 432 belonging to user with admin rights.
433 433 If "perm" is None, user will be removed from the repository.
434 434
435 435 INPUT::
436 436
437 437 api_key : "<api_key>"
438 438 method : "add_user_to_repo"
439 439 args: {
440 440 "repo_name" : "<reponame>",
441 441 "username" : "<username>",
442 442 "perm" : "(None|repository.(read|write|admin))",
443 443 }
444 444
445 445 OUTPUT::
446 446
447 447 result: {
448 448 "msg" : "Added perm: <perm> for <username> in repo: <reponame>"
449 449 }
450 450 error: null
451 451
452 452 add_users_group_to_repo
453 453 -----------------------
454 454
455 455 Add a users group to a repository. This command can be executed only using
456 456 api_key belonging to user with admin rights. If "perm" is None, group will
457 457 be removed from the repository.
458 458
459 459 INPUT::
460 460
461 461 api_key : "<api_key>"
462 462 method : "add_users_group_to_repo"
463 463 args: {
464 464 "repo_name" : "<reponame>",
465 465 "group_name" : "<groupname>",
466 466 "perm" : "(None|repository.(read|write|admin))",
467 467 }
468 468 OUTPUT::
469 469
470 470 result: {
471 471 "msg" : Added perm: <perm> for <groupname> in repo: <reponame>"
472 472 }
473 473
@@ -1,478 +1,479 b''
1 1 .. _changelog:
2 2
3 3 Changelog
4 4 =========
5 5
6 6
7 7 1.3.0 (**XXXX-XX-XX**)
8 8 ======================
9 9
10 10 :status: in-progress
11 11 :branch: beta
12 12
13 13 news
14 14 ----
15 15
16 16 - code review, inspired by github code-comments
17 17 - #215 rst and markdown README files support
18 18 - #252 Container-based and proxy pass-through authentication support
19 19 - #44 branch browser. Filtering of changelog by branches
20 20 - mercurial bookmarks support
21 21 - new hover top menu, optimized to add maximum size for important views
22 22 - configurable clone url template with possibility to specify protocol like
23 23 ssh:// or http:// and also manually alter other parts of clone_url.
24 24 - enabled largefiles extension by default
25 25 - optimized summary file pages and saved a lot of unused space in them
26 26 - #239 option to manually mark repository as fork
27 27 - #320 mapping of commit authors to RhodeCode users
28 28 - #304 hashes are displayed using monospace font
29 29 - diff configuration, toggle white lines and context lines
30 30 - #307 configurable diffs, whitespace toggle, increasing context lines
31 31 - sorting on branches, tags and bookmarks using YUI datatable
32 32 - improved file filter on files page
33 33 - implements #330 api method for listing nodes ar particular revision
34 34 - #73 added linking issues in commit messages to chosen issue tracker url
35 35 based on user defined regular expression
36 36 - added linking of changesets in commit messages
37 37 - new compact changelog with expandable commit messages
38 - firstname and lastname are optional in user creation
38 39
39 40 fixes
40 41 -----
41 42
42 43 - rewrote dbsession management for atomic operations, and better error handling
43 44 - fixed sorting of repo tables
44 45 - #326 escape of special html entities in diffs
45 46 - normalized user_name => username in api attributes
46 47 - fixes #298 ldap created users with mixed case emails created conflicts
47 48 on saving a form
48 49 - fixes issue when owner of a repo couldn't revoke permissions for users
49 50 and groups
50 51 - fixes #271 rare JSON serialization problem with statistics
51 52 - fixes #337 missing validation check for conflicting names of a group with a
52 53 repositories group
53 54 - #340 fixed session problem for mysql and celery tasks
54 55 - fixed #331 RhodeCode mangles repository names if the a repository group
55 56 contains the "full path" to the repositories
56 57
57 58
58 59 1.2.4 (**2012-01-19**)
59 60 ======================
60 61
61 62 news
62 63 ----
63 64
64 65 - RhodeCode is bundled with mercurial series 2.0.X by default, with
65 66 full support to largefiles extension. Enabled by default in new installations
66 67 - #329 Ability to Add/Remove Groups to/from a Repository via AP
67 68 - added requires.txt file with requirements
68 69
69 70 fixes
70 71 -----
71 72
72 73 - fixes db session issues with celery when emailing admins
73 74 - #331 RhodeCode mangles repository names if the a repository group
74 75 contains the "full path" to the repositories
75 76 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
76 77 - DB session cleanup after hg protocol operations, fixes issues with
77 78 `mysql has gone away` errors
78 79 - #333 doc fixes for get_repo api function
79 80 - #271 rare JSON serialization problem with statistics enabled
80 81 - #337 Fixes issues with validation of repository name conflicting with
81 82 a group name. A proper message is now displayed.
82 83 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
83 84 doesn't work
84 85 - #316 fixes issues with web description in hgrc files
85 86
86 87 1.2.3 (**2011-11-02**)
87 88 ======================
88 89
89 90 news
90 91 ----
91 92
92 93 - added option to manage repos group for non admin users
93 94 - added following API methods for get_users, create_user, get_users_groups,
94 95 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
95 96 get_repo, create_repo, add_user_to_repo
96 97 - implements #237 added password confirmation for my account
97 98 and admin edit user.
98 99 - implements #291 email notification for global events are now sent to all
99 100 administrator users, and global config email.
100 101
101 102 fixes
102 103 -----
103 104
104 105 - added option for passing auth method for smtp mailer
105 106 - #276 issue with adding a single user with id>10 to usergroups
106 107 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
107 108 - #288 fixes managing of repos in a group for non admin user
108 109
109 110 1.2.2 (**2011-10-17**)
110 111 ======================
111 112
112 113 news
113 114 ----
114 115
115 116 - #226 repo groups are available by path instead of numerical id
116 117
117 118 fixes
118 119 -----
119 120
120 121 - #259 Groups with the same name but with different parent group
121 122 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
122 123 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
123 124 - #265 ldap save fails sometimes on converting attributes to booleans,
124 125 added getter and setter into model that will prevent from this on db model level
125 126 - fixed problems with timestamps issues #251 and #213
126 127 - fixes #266 RhodeCode allows to create repo with the same name and in
127 128 the same parent as group
128 129 - fixes #245 Rescan of the repositories on Windows
129 130 - fixes #248 cannot edit repos inside a group on windows
130 131 - fixes #219 forking problems on windows
131 132
132 133 1.2.1 (**2011-10-08**)
133 134 ======================
134 135
135 136 news
136 137 ----
137 138
138 139
139 140 fixes
140 141 -----
141 142
142 143 - fixed problems with basic auth and push problems
143 144 - gui fixes
144 145 - fixed logger
145 146
146 147 1.2.0 (**2011-10-07**)
147 148 ======================
148 149
149 150 news
150 151 ----
151 152
152 153 - implemented #47 repository groups
153 154 - implemented #89 Can setup google analytics code from settings menu
154 155 - implemented #91 added nicer looking archive urls with more download options
155 156 like tags, branches
156 157 - implemented #44 into file browsing, and added follow branch option
157 158 - implemented #84 downloads can be enabled/disabled for each repository
158 159 - anonymous repository can be cloned without having to pass default:default
159 160 into clone url
160 161 - fixed #90 whoosh indexer can index chooses repositories passed in command
161 162 line
162 163 - extended journal with day aggregates and paging
163 164 - implemented #107 source code lines highlight ranges
164 165 - implemented #93 customizable changelog on combined revision ranges -
165 166 equivalent of githubs compare view
166 167 - implemented #108 extended and more powerful LDAP configuration
167 168 - implemented #56 users groups
168 169 - major code rewrites optimized codes for speed and memory usage
169 170 - raw and diff downloads are now in git format
170 171 - setup command checks for write access to given path
171 172 - fixed many issues with international characters and unicode. It uses utf8
172 173 decode with replace to provide less errors even with non utf8 encoded strings
173 174 - #125 added API KEY access to feeds
174 175 - #109 Repository can be created from external Mercurial link (aka. remote
175 176 repository, and manually updated (via pull) from admin panel
176 177 - beta git support - push/pull server + basic view for git repos
177 178 - added followers page and forks page
178 179 - server side file creation (with binary file upload interface)
179 180 and edition with commits powered by codemirror
180 181 - #111 file browser file finder, quick lookup files on whole file tree
181 182 - added quick login sliding menu into main page
182 183 - changelog uses lazy loading of affected files details, in some scenarios
183 184 this can improve speed of changelog page dramatically especially for
184 185 larger repositories.
185 186 - implements #214 added support for downloading subrepos in download menu.
186 187 - Added basic API for direct operations on rhodecode via JSON
187 188 - Implemented advanced hook management
188 189
189 190 fixes
190 191 -----
191 192
192 193 - fixed file browser bug, when switching into given form revision the url was
193 194 not changing
194 195 - fixed propagation to error controller on simplehg and simplegit middlewares
195 196 - fixed error when trying to make a download on empty repository
196 197 - fixed problem with '[' chars in commit messages in journal
197 198 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
198 199 - journal fork fixes
199 200 - removed issue with space inside renamed repository after deletion
200 201 - fixed strange issue on formencode imports
201 202 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
202 203 - #150 fixes for errors on repositories mapped in db but corrupted in
203 204 filesystem
204 205 - fixed problem with ascendant characters in realm #181
205 206 - fixed problem with sqlite file based database connection pool
206 207 - whoosh indexer and code stats share the same dynamic extensions map
207 208 - fixes #188 - relationship delete of repo_to_perm entry on user removal
208 209 - fixes issue #189 Trending source files shows "show more" when no more exist
209 210 - fixes issue #197 Relative paths for pidlocks
210 211 - fixes issue #198 password will require only 3 chars now for login form
211 212 - fixes issue #199 wrong redirection for non admin users after creating a repository
212 213 - fixes issues #202, bad db constraint made impossible to attach same group
213 214 more than one time. Affects only mysql/postgres
214 215 - fixes #218 os.kill patch for windows was missing sig param
215 216 - improved rendering of dag (they are not trimmed anymore when number of
216 217 heads exceeds 5)
217 218
218 219 1.1.8 (**2011-04-12**)
219 220 ======================
220 221
221 222 news
222 223 ----
223 224
224 225 - improved windows support
225 226
226 227 fixes
227 228 -----
228 229
229 230 - fixed #140 freeze of python dateutil library, since new version is python2.x
230 231 incompatible
231 232 - setup-app will check for write permission in given path
232 233 - cleaned up license info issue #149
233 234 - fixes for issues #137,#116 and problems with unicode and accented characters.
234 235 - fixes crashes on gravatar, when passed in email as unicode
235 236 - fixed tooltip flickering problems
236 237 - fixed came_from redirection on windows
237 238 - fixed logging modules, and sql formatters
238 239 - windows fixes for os.kill issue #133
239 240 - fixes path splitting for windows issues #148
240 241 - fixed issue #143 wrong import on migration to 1.1.X
241 242 - fixed problems with displaying binary files, thanks to Thomas Waldmann
242 243 - removed name from archive files since it's breaking ui for long repo names
243 244 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
244 245 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
245 246 Thomas Waldmann
246 247 - fixed issue #166 summary pager was skipping 10 revisions on second page
247 248
248 249
249 250 1.1.7 (**2011-03-23**)
250 251 ======================
251 252
252 253 news
253 254 ----
254 255
255 256 fixes
256 257 -----
257 258
258 259 - fixed (again) #136 installation support for FreeBSD
259 260
260 261
261 262 1.1.6 (**2011-03-21**)
262 263 ======================
263 264
264 265 news
265 266 ----
266 267
267 268 fixes
268 269 -----
269 270
270 271 - fixed #136 installation support for FreeBSD
271 272 - RhodeCode will check for python version during installation
272 273
273 274 1.1.5 (**2011-03-17**)
274 275 ======================
275 276
276 277 news
277 278 ----
278 279
279 280 - basic windows support, by exchanging pybcrypt into sha256 for windows only
280 281 highly inspired by idea of mantis406
281 282
282 283 fixes
283 284 -----
284 285
285 286 - fixed sorting by author in main page
286 287 - fixed crashes with diffs on binary files
287 288 - fixed #131 problem with boolean values for LDAP
288 289 - fixed #122 mysql problems thanks to striker69
289 290 - fixed problem with errors on calling raw/raw_files/annotate functions
290 291 with unknown revisions
291 292 - fixed returned rawfiles attachment names with international character
292 293 - cleaned out docs, big thanks to Jason Harris
293 294
294 295 1.1.4 (**2011-02-19**)
295 296 ======================
296 297
297 298 news
298 299 ----
299 300
300 301 fixes
301 302 -----
302 303
303 304 - fixed formencode import problem on settings page, that caused server crash
304 305 when that page was accessed as first after server start
305 306 - journal fixes
306 307 - fixed option to access repository just by entering http://server/<repo_name>
307 308
308 309 1.1.3 (**2011-02-16**)
309 310 ======================
310 311
311 312 news
312 313 ----
313 314
314 315 - implemented #102 allowing the '.' character in username
315 316 - added option to access repository just by entering http://server/<repo_name>
316 317 - celery task ignores result for better performance
317 318
318 319 fixes
319 320 -----
320 321
321 322 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
322 323 apollo13 and Johan Walles
323 324 - small fixes in journal
324 325 - fixed problems with getting setting for celery from .ini files
325 326 - registration, password reset and login boxes share the same title as main
326 327 application now
327 328 - fixed #113: to high permissions to fork repository
328 329 - fixed problem with '[' chars in commit messages in journal
329 330 - removed issue with space inside renamed repository after deletion
330 331 - db transaction fixes when filesystem repository creation failed
331 332 - fixed #106 relation issues on databases different than sqlite
332 333 - fixed static files paths links to use of url() method
333 334
334 335 1.1.2 (**2011-01-12**)
335 336 ======================
336 337
337 338 news
338 339 ----
339 340
340 341
341 342 fixes
342 343 -----
343 344
344 345 - fixes #98 protection against float division of percentage stats
345 346 - fixed graph bug
346 347 - forced webhelpers version since it was making troubles during installation
347 348
348 349 1.1.1 (**2011-01-06**)
349 350 ======================
350 351
351 352 news
352 353 ----
353 354
354 355 - added force https option into ini files for easier https usage (no need to
355 356 set server headers with this options)
356 357 - small css updates
357 358
358 359 fixes
359 360 -----
360 361
361 362 - fixed #96 redirect loop on files view on repositories without changesets
362 363 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
363 364 and server crashed with errors
364 365 - fixed large tooltips problems on main page
365 366 - fixed #92 whoosh indexer is more error proof
366 367
367 368 1.1.0 (**2010-12-18**)
368 369 ======================
369 370
370 371 news
371 372 ----
372 373
373 374 - rewrite of internals for vcs >=0.1.10
374 375 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
375 376 with older clients
376 377 - anonymous access, authentication via ldap
377 378 - performance upgrade for cached repos list - each repository has its own
378 379 cache that's invalidated when needed.
379 380 - performance upgrades on repositories with large amount of commits (20K+)
380 381 - main page quick filter for filtering repositories
381 382 - user dashboards with ability to follow chosen repositories actions
382 383 - sends email to admin on new user registration
383 384 - added cache/statistics reset options into repository settings
384 385 - more detailed action logger (based on hooks) with pushed changesets lists
385 386 and options to disable those hooks from admin panel
386 387 - introduced new enhanced changelog for merges that shows more accurate results
387 388 - new improved and faster code stats (based on pygments lexers mapping tables,
388 389 showing up to 10 trending sources for each repository. Additionally stats
389 390 can be disabled in repository settings.
390 391 - gui optimizations, fixed application width to 1024px
391 392 - added cut off (for large files/changesets) limit into config files
392 393 - whoosh, celeryd, upgrade moved to paster command
393 394 - other than sqlite database backends can be used
394 395
395 396 fixes
396 397 -----
397 398
398 399 - fixes #61 forked repo was showing only after cache expired
399 400 - fixes #76 no confirmation on user deletes
400 401 - fixes #66 Name field misspelled
401 402 - fixes #72 block user removal when he owns repositories
402 403 - fixes #69 added password confirmation fields
403 404 - fixes #87 RhodeCode crashes occasionally on updating repository owner
404 405 - fixes #82 broken annotations on files with more than 1 blank line at the end
405 406 - a lot of fixes and tweaks for file browser
406 407 - fixed detached session issues
407 408 - fixed when user had no repos he would see all repos listed in my account
408 409 - fixed ui() instance bug when global hgrc settings was loaded for server
409 410 instance and all hgrc options were merged with our db ui() object
410 411 - numerous small bugfixes
411 412
412 413 (special thanks for TkSoh for detailed feedback)
413 414
414 415
415 416 1.0.2 (**2010-11-12**)
416 417 ======================
417 418
418 419 news
419 420 ----
420 421
421 422 - tested under python2.7
422 423 - bumped sqlalchemy and celery versions
423 424
424 425 fixes
425 426 -----
426 427
427 428 - fixed #59 missing graph.js
428 429 - fixed repo_size crash when repository had broken symlinks
429 430 - fixed python2.5 crashes.
430 431
431 432
432 433 1.0.1 (**2010-11-10**)
433 434 ======================
434 435
435 436 news
436 437 ----
437 438
438 439 - small css updated
439 440
440 441 fixes
441 442 -----
442 443
443 444 - fixed #53 python2.5 incompatible enumerate calls
444 445 - fixed #52 disable mercurial extension for web
445 446 - fixed #51 deleting repositories don't delete it's dependent objects
446 447
447 448
448 449 1.0.0 (**2010-11-02**)
449 450 ======================
450 451
451 452 - security bugfix simplehg wasn't checking for permissions on commands
452 453 other than pull or push.
453 454 - fixed doubled messages after push or pull in admin journal
454 455 - templating and css corrections, fixed repo switcher on chrome, updated titles
455 456 - admin menu accessible from options menu on repository view
456 457 - permissions cached queries
457 458
458 459 1.0.0rc4 (**2010-10-12**)
459 460 ==========================
460 461
461 462 - fixed python2.5 missing simplejson imports (thanks to Jens BΓ€ckman)
462 463 - removed cache_manager settings from sqlalchemy meta
463 464 - added sqlalchemy cache settings to ini files
464 465 - validated password length and added second try of failure on paster setup-app
465 466 - fixed setup database destroy prompt even when there was no db
466 467
467 468
468 469 1.0.0rc3 (**2010-10-11**)
469 470 =========================
470 471
471 472 - fixed i18n during installation.
472 473
473 474 1.0.0rc2 (**2010-10-11**)
474 475 =========================
475 476
476 477 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
477 478 occure. After vcs is fixed it'll be put back again.
478 479 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,510 +1,510 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 API controller for RhodeCode
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import traceback
29 29 import logging
30 30
31 31 from sqlalchemy.orm.exc import NoResultFound
32 32
33 33 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
34 34 from rhodecode.lib.auth import HasPermissionAllDecorator, \
35 35 HasPermissionAnyDecorator
36 36
37 37 from rhodecode.model.meta import Session
38 38 from rhodecode.model.scm import ScmModel
39 39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.repo_permission import RepositoryPermissionModel
43 43 from rhodecode.model.users_group import UsersGroupModel
44 44 from rhodecode.model.repos_group import ReposGroupModel
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class ApiController(JSONRPCController):
51 51 """
52 52 API Controller
53 53
54 54
55 55 Each method needs to have USER as argument this is then based on given
56 56 API_KEY propagated as instance of user object
57 57
58 58 Preferably this should be first argument also
59 59
60 60
61 61 Each function should also **raise** JSONRPCError for any
62 62 errors that happens
63 63
64 64 """
65 65
66 66 @HasPermissionAllDecorator('hg.admin')
67 67 def pull(self, apiuser, repo_name):
68 68 """
69 69 Dispatch pull action on given repo
70 70
71 71
72 72 :param user:
73 73 :param repo_name:
74 74 """
75 75
76 76 if Repository.is_valid(repo_name) is False:
77 77 raise JSONRPCError('Unknown repo "%s"' % repo_name)
78 78
79 79 try:
80 80 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
81 81 return 'Pulled from %s' % repo_name
82 82 except Exception:
83 83 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
84 84
85 85 @HasPermissionAllDecorator('hg.admin')
86 86 def get_user(self, apiuser, username):
87 87 """"
88 88 Get a user by username
89 89
90 90 :param apiuser:
91 91 :param username:
92 92 """
93 93
94 94 user = User.get_by_username(username)
95 95 if not user:
96 96 return None
97 97
98 98 return dict(
99 99 id=user.user_id,
100 100 username=user.username,
101 101 firstname=user.name,
102 102 lastname=user.lastname,
103 103 email=user.email,
104 104 active=user.active,
105 105 admin=user.admin,
106 106 ldap=user.ldap_dn
107 107 )
108 108
109 109 @HasPermissionAllDecorator('hg.admin')
110 110 def get_users(self, apiuser):
111 111 """"
112 112 Get all users
113 113
114 114 :param apiuser:
115 115 """
116 116
117 117 result = []
118 118 for user in User.getAll():
119 119 result.append(
120 120 dict(
121 121 id=user.user_id,
122 122 username=user.username,
123 123 firstname=user.name,
124 124 lastname=user.lastname,
125 125 email=user.email,
126 126 active=user.active,
127 127 admin=user.admin,
128 128 ldap=user.ldap_dn
129 129 )
130 130 )
131 131 return result
132 132
133 133 @HasPermissionAllDecorator('hg.admin')
134 def create_user(self, apiuser, username, password, firstname,
135 lastname, email, active=True, admin=False, ldap_dn=None):
134 def create_user(self, apiuser, username, password, email, firstname=None,
135 lastname=None, active=True, admin=False, ldap_dn=None):
136 136 """
137 137 Create new user or updates current one
138 138
139 139 :param apiuser:
140 140 :param username:
141 141 :param password:
142 :param email:
142 143 :param name:
143 144 :param lastname:
144 :param email:
145 145 :param active:
146 146 :param admin:
147 147 :param ldap_dn:
148 148 """
149 149
150 150 if User.get_by_username(username):
151 151 raise JSONRPCError("user %s already exist" % username)
152 152
153 153 try:
154 154 usr = UserModel().create_or_update(
155 155 username, password, email, firstname,
156 156 lastname, active, admin, ldap_dn
157 157 )
158 158 Session.commit()
159 159 return dict(
160 160 id=usr.user_id,
161 161 msg='created new user %s' % username
162 162 )
163 163 except Exception:
164 164 log.error(traceback.format_exc())
165 165 raise JSONRPCError('failed to create user %s' % username)
166 166
167 167 @HasPermissionAllDecorator('hg.admin')
168 168 def get_users_group(self, apiuser, group_name):
169 169 """"
170 170 Get users group by name
171 171
172 172 :param apiuser:
173 173 :param group_name:
174 174 """
175 175
176 176 users_group = UsersGroup.get_by_group_name(group_name)
177 177 if not users_group:
178 178 return None
179 179
180 180 members = []
181 181 for user in users_group.members:
182 182 user = user.user
183 183 members.append(dict(id=user.user_id,
184 184 username=user.username,
185 185 firstname=user.name,
186 186 lastname=user.lastname,
187 187 email=user.email,
188 188 active=user.active,
189 189 admin=user.admin,
190 190 ldap=user.ldap_dn))
191 191
192 192 return dict(id=users_group.users_group_id,
193 193 group_name=users_group.users_group_name,
194 194 active=users_group.users_group_active,
195 195 members=members)
196 196
197 197 @HasPermissionAllDecorator('hg.admin')
198 198 def get_users_groups(self, apiuser):
199 199 """"
200 200 Get all users groups
201 201
202 202 :param apiuser:
203 203 """
204 204
205 205 result = []
206 206 for users_group in UsersGroup.getAll():
207 207 members = []
208 208 for user in users_group.members:
209 209 user = user.user
210 210 members.append(dict(id=user.user_id,
211 211 username=user.username,
212 212 firstname=user.name,
213 213 lastname=user.lastname,
214 214 email=user.email,
215 215 active=user.active,
216 216 admin=user.admin,
217 217 ldap=user.ldap_dn))
218 218
219 219 result.append(dict(id=users_group.users_group_id,
220 220 group_name=users_group.users_group_name,
221 221 active=users_group.users_group_active,
222 222 members=members))
223 223 return result
224 224
225 225 @HasPermissionAllDecorator('hg.admin')
226 226 def create_users_group(self, apiuser, group_name, active=True):
227 227 """
228 228 Creates an new usergroup
229 229
230 230 :param group_name:
231 231 :param active:
232 232 """
233 233
234 234 if self.get_users_group(apiuser, group_name):
235 235 raise JSONRPCError("users group %s already exist" % group_name)
236 236
237 237 try:
238 238 ug = UsersGroupModel().create(name=group_name, active=active)
239 239 Session.commit()
240 240 return dict(id=ug.users_group_id,
241 241 msg='created new users group %s' % group_name)
242 242 except Exception:
243 243 log.error(traceback.format_exc())
244 244 raise JSONRPCError('failed to create group %s' % group_name)
245 245
246 246 @HasPermissionAllDecorator('hg.admin')
247 247 def add_user_to_users_group(self, apiuser, group_name, username):
248 248 """"
249 249 Add a user to a group
250 250
251 251 :param apiuser:
252 252 :param group_name:
253 253 :param username:
254 254 """
255 255
256 256 try:
257 257 users_group = UsersGroup.get_by_group_name(group_name)
258 258 if not users_group:
259 259 raise JSONRPCError('unknown users group %s' % group_name)
260 260
261 261 try:
262 262 user = User.get_by_username(username)
263 263 except NoResultFound:
264 264 raise JSONRPCError('unknown user %s' % username)
265 265
266 266 ugm = UsersGroupModel().add_user_to_group(users_group, user)
267 267 Session.commit()
268 268 return dict(id=ugm.users_group_member_id,
269 269 msg='created new users group member')
270 270 except Exception:
271 271 log.error(traceback.format_exc())
272 272 raise JSONRPCError('failed to create users group member')
273 273
274 274 @HasPermissionAnyDecorator('hg.admin')
275 275 def get_repo(self, apiuser, repo_name):
276 276 """"
277 277 Get repository by name
278 278
279 279 :param apiuser:
280 280 :param repo_name:
281 281 """
282 282
283 283 repo = Repository.get_by_repo_name(repo_name)
284 284 if repo is None:
285 285 raise JSONRPCError('unknown repository %s' % repo)
286 286
287 287 members = []
288 288 for user in repo.repo_to_perm:
289 289 perm = user.permission.permission_name
290 290 user = user.user
291 291 members.append(
292 292 dict(
293 293 type_="user",
294 294 id=user.user_id,
295 295 username=user.username,
296 296 firstname=user.name,
297 297 lastname=user.lastname,
298 298 email=user.email,
299 299 active=user.active,
300 300 admin=user.admin,
301 301 ldap=user.ldap_dn,
302 302 permission=perm
303 303 )
304 304 )
305 305 for users_group in repo.users_group_to_perm:
306 306 perm = users_group.permission.permission_name
307 307 users_group = users_group.users_group
308 308 members.append(
309 309 dict(
310 310 type_="users_group",
311 311 id=users_group.users_group_id,
312 312 name=users_group.users_group_name,
313 313 active=users_group.users_group_active,
314 314 permission=perm
315 315 )
316 316 )
317 317
318 318 return dict(
319 319 id=repo.repo_id,
320 320 repo_name=repo.repo_name,
321 321 type=repo.repo_type,
322 322 description=repo.description,
323 323 members=members
324 324 )
325 325
326 326 @HasPermissionAnyDecorator('hg.admin')
327 327 def get_repos(self, apiuser):
328 328 """"
329 329 Get all repositories
330 330
331 331 :param apiuser:
332 332 """
333 333
334 334 result = []
335 335 for repository in Repository.getAll():
336 336 result.append(
337 337 dict(
338 338 id=repository.repo_id,
339 339 repo_name=repository.repo_name,
340 340 type=repository.repo_type,
341 341 description=repository.description
342 342 )
343 343 )
344 344 return result
345 345
346 346 @HasPermissionAnyDecorator('hg.admin')
347 347 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
348 348 ret_type='all'):
349 349 """
350 350 returns a list of nodes and it's children
351 351 for a given path at given revision. It's possible to specify ret_type
352 352 to show only files or dirs
353 353
354 354 :param apiuser:
355 355 :param repo_name: name of repository
356 356 :param revision: revision for which listing should be done
357 357 :param root_path: path from which start displaying
358 358 :param ret_type: return type 'all|files|dirs' nodes
359 359 """
360 360 try:
361 361 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
362 362 flat=False)
363 363 _map = {
364 364 'all': _d + _f,
365 365 'files': _f,
366 366 'dirs': _d,
367 367 }
368 368 return _map[ret_type]
369 369 except KeyError:
370 370 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
371 371 except Exception, e:
372 372 raise JSONRPCError(e)
373 373
374 374 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
375 375 def create_repo(self, apiuser, repo_name, owner_name, description='',
376 376 repo_type='hg', private=False):
377 377 """
378 378 Create a repository
379 379
380 380 :param apiuser:
381 381 :param repo_name:
382 382 :param description:
383 383 :param type:
384 384 :param private:
385 385 :param owner_name:
386 386 """
387 387
388 388 try:
389 389 try:
390 390 owner = User.get_by_username(owner_name)
391 391 except NoResultFound:
392 392 raise JSONRPCError('unknown user %s' % owner)
393 393
394 394 if Repository.get_by_repo_name(repo_name):
395 395 raise JSONRPCError("repo %s already exist" % repo_name)
396 396
397 397 groups = repo_name.split('/')
398 398 real_name = groups[-1]
399 399 groups = groups[:-1]
400 400 parent_id = None
401 401 for g in groups:
402 402 group = RepoGroup.get_by_group_name(g)
403 403 if not group:
404 404 group = ReposGroupModel().create(
405 405 dict(
406 406 group_name=g,
407 407 group_description='',
408 408 group_parent_id=parent_id
409 409 )
410 410 )
411 411 parent_id = group.group_id
412 412
413 413 repo = RepoModel().create(
414 414 dict(
415 415 repo_name=real_name,
416 416 repo_name_full=repo_name,
417 417 description=description,
418 418 private=private,
419 419 repo_type=repo_type,
420 420 repo_group=parent_id,
421 421 clone_uri=None
422 422 ),
423 423 owner
424 424 )
425 425 Session.commit()
426 426
427 427 return dict(
428 428 id=repo.repo_id,
429 429 msg="Created new repository %s" % repo.repo_name
430 430 )
431 431
432 432 except Exception:
433 433 log.error(traceback.format_exc())
434 434 raise JSONRPCError('failed to create repository %s' % repo_name)
435 435
436 436 @HasPermissionAnyDecorator('hg.admin')
437 437 def add_user_to_repo(self, apiuser, repo_name, username, perm):
438 438 """
439 439 Add permission for a user to a repository
440 440
441 441 :param apiuser:
442 442 :param repo_name:
443 443 :param username:
444 444 :param perm:
445 445 """
446 446
447 447 try:
448 448 repo = Repository.get_by_repo_name(repo_name)
449 449 if repo is None:
450 450 raise JSONRPCError('unknown repository %s' % repo)
451 451
452 452 try:
453 453 user = User.get_by_username(username)
454 454 except NoResultFound:
455 455 raise JSONRPCError('unknown user %s' % user)
456 456
457 457 RepositoryPermissionModel()\
458 458 .update_or_delete_user_permission(repo, user, perm)
459 459 Session.commit()
460 460
461 461 return dict(
462 462 msg='Added perm: %s for %s in repo: %s' % (
463 463 perm, username, repo_name
464 464 )
465 465 )
466 466 except Exception:
467 467 log.error(traceback.format_exc())
468 468 raise JSONRPCError(
469 469 'failed to edit permission %(repo)s for %(user)s' % dict(
470 470 user=username, repo=repo_name
471 471 )
472 472 )
473 473
474 474 @HasPermissionAnyDecorator('hg.admin')
475 475 def add_users_group_to_repo(self, apiuser, repo_name, group_name, perm):
476 476 """
477 477 Add permission for a users group to a repository
478 478
479 479 :param apiuser:
480 480 :param repo_name:
481 481 :param group_name:
482 482 :param perm:
483 483 """
484 484
485 485 try:
486 486 repo = Repository.get_by_repo_name(repo_name)
487 487 if repo is None:
488 488 raise JSONRPCError('unknown repository %s' % repo)
489 489
490 490 try:
491 491 user_group = UsersGroup.get_by_group_name(group_name)
492 492 except NoResultFound:
493 493 raise JSONRPCError('unknown users group %s' % user_group)
494 494
495 495 RepositoryPermissionModel()\
496 496 .update_or_delete_users_group_permission(repo, user_group,
497 497 perm)
498 498 Session.commit()
499 499 return dict(
500 500 msg='Added perm: %s for %s in repo: %s' % (
501 501 perm, group_name, repo_name
502 502 )
503 503 )
504 504 except Exception:
505 505 log.error(traceback.format_exc())
506 506 raise JSONRPCError(
507 507 'failed to edit permission %(repo)s for %(usergr)s' % dict(
508 508 usergr=group_name, repo=repo_name
509 509 )
510 510 )
@@ -1,699 +1,701 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, session, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 39 from rhodecode.model.meta import Session
40 40
41 41 if __platform__ in PLATFORM_WIN:
42 42 from hashlib import sha256
43 43 if __platform__ in PLATFORM_OTHERS:
44 44 import bcrypt
45 45
46 46 from rhodecode.lib import str2bool, safe_unicode
47 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 48 from rhodecode.lib.utils import get_repo_slug
49 49 from rhodecode.lib.auth_ldap import AuthLdap
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class PasswordGenerator(object):
59 59 """
60 60 This is a simple class for generating password from different sets of
61 61 characters
62 62 usage::
63 63
64 64 passwd_gen = PasswordGenerator()
65 65 #print 8-letter password containing only big and small letters
66 66 of alphabet
67 67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
68 68 """
69 69 ALPHABETS_NUM = r'''1234567890'''
70 70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
71 71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
72 72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
73 73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
74 74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
75 75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
76 76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
77 77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
78 78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
79 79
80 80 def __init__(self, passwd=''):
81 81 self.passwd = passwd
82 82
83 83 def gen_password(self, len, type):
84 84 self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
85 85 return self.passwd
86 86
87 87
88 88 class RhodeCodeCrypto(object):
89 89
90 90 @classmethod
91 91 def hash_string(cls, str_):
92 92 """
93 93 Cryptographic function used for password hashing based on pybcrypt
94 94 or pycrypto in windows
95 95
96 96 :param password: password to hash
97 97 """
98 98 if __platform__ in PLATFORM_WIN:
99 99 return sha256(str_).hexdigest()
100 100 elif __platform__ in PLATFORM_OTHERS:
101 101 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
102 102 else:
103 103 raise Exception('Unknown or unsupported platform %s' \
104 104 % __platform__)
105 105
106 106 @classmethod
107 107 def hash_check(cls, password, hashed):
108 108 """
109 109 Checks matching password with it's hashed value, runs different
110 110 implementation based on platform it runs on
111 111
112 112 :param password: password
113 113 :param hashed: password in hashed form
114 114 """
115 115
116 116 if __platform__ in PLATFORM_WIN:
117 117 return sha256(password).hexdigest() == hashed
118 118 elif __platform__ in PLATFORM_OTHERS:
119 119 return bcrypt.hashpw(password, hashed) == hashed
120 120 else:
121 121 raise Exception('Unknown or unsupported platform %s' \
122 122 % __platform__)
123 123
124 124
125 125 def get_crypt_password(password):
126 126 return RhodeCodeCrypto.hash_string(password)
127 127
128 128
129 129 def check_password(password, hashed):
130 130 return RhodeCodeCrypto.hash_check(password, hashed)
131 131
132
132 133 def generate_api_key(str_, salt=None):
133 134 """
134 135 Generates API KEY from given string
135 136
136 137 :param str_:
137 138 :param salt:
138 139 """
139 140
140 141 if salt is None:
141 142 salt = _RandomNameSequence().next()
142 143
143 144 return hashlib.sha1(str_ + salt).hexdigest()
144 145
145 146
146 147 def authfunc(environ, username, password):
147 148 """
148 149 Dummy authentication wrapper function used in Mercurial and Git for
149 150 access control.
150 151
151 152 :param environ: needed only for using in Basic auth
152 153 """
153 154 return authenticate(username, password)
154 155
155 156
156 157 def authenticate(username, password):
157 158 """
158 159 Authentication function used for access control,
159 160 firstly checks for db authentication then if ldap is enabled for ldap
160 161 authentication, also creates ldap user if not in database
161 162
162 163 :param username: username
163 164 :param password: password
164 165 """
165 166
166 167 user_model = UserModel()
167 168 user = User.get_by_username(username)
168 169
169 170 log.debug('Authenticating user using RhodeCode account')
170 171 if user is not None and not user.ldap_dn:
171 172 if user.active:
172 173 if user.username == 'default' and user.active:
173 174 log.info('user %s authenticated correctly as anonymous user',
174 175 username)
175 176 return True
176 177
177 178 elif user.username == username and check_password(password,
178 179 user.password):
179 180 log.info('user %s authenticated correctly', username)
180 181 return True
181 182 else:
182 183 log.warning('user %s is disabled', username)
183 184
184 185 else:
185 186 log.debug('Regular authentication failed')
186 187 user_obj = User.get_by_username(username, case_insensitive=True)
187 188
188 189 if user_obj is not None and not user_obj.ldap_dn:
189 190 log.debug('this user already exists as non ldap')
190 191 return False
191 192
192 193 ldap_settings = RhodeCodeSetting.get_ldap_settings()
193 194 #======================================================================
194 195 # FALLBACK TO LDAP AUTH IF ENABLE
195 196 #======================================================================
196 197 if str2bool(ldap_settings.get('ldap_active')):
197 198 log.debug("Authenticating user using ldap")
198 199 kwargs = {
199 200 'server': ldap_settings.get('ldap_host', ''),
200 201 'base_dn': ldap_settings.get('ldap_base_dn', ''),
201 202 'port': ldap_settings.get('ldap_port'),
202 203 'bind_dn': ldap_settings.get('ldap_dn_user'),
203 204 'bind_pass': ldap_settings.get('ldap_dn_pass'),
204 205 'tls_kind': ldap_settings.get('ldap_tls_kind'),
205 206 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
206 207 'ldap_filter': ldap_settings.get('ldap_filter'),
207 208 'search_scope': ldap_settings.get('ldap_search_scope'),
208 209 'attr_login': ldap_settings.get('ldap_attr_login'),
209 210 'ldap_version': 3,
210 211 }
211 212 log.debug('Checking for ldap authentication')
212 213 try:
213 214 aldap = AuthLdap(**kwargs)
214 215 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
215 216 password)
216 217 log.debug('Got ldap DN response %s', user_dn)
217 218
218 219 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
219 220 .get(k), [''])[0]
220 221
221 222 user_attrs = {
222 223 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
223 224 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
224 225 'email': get_ldap_attr('ldap_attr_email'),
225 226 }
226 227
227 228 if user_model.create_ldap(username, password, user_dn,
228 229 user_attrs):
229 230 log.info('created new ldap user %s', username)
230 231
231 232 Session.commit()
232 233 return True
233 234 except (LdapUsernameError, LdapPasswordError,):
234 235 pass
235 236 except (Exception,):
236 237 log.error(traceback.format_exc())
237 238 pass
238 239 return False
239 240
241
240 242 def login_container_auth(username):
241 243 user = User.get_by_username(username)
242 244 if user is None:
243 245 user_attrs = {
244 246 'name': username,
245 247 'lastname': None,
246 248 'email': None,
247 249 }
248 250 user = UserModel().create_for_container_auth(username, user_attrs)
249 251 if not user:
250 252 return None
251 253 log.info('User %s was created by container authentication', username)
252 254
253 255 if not user.active:
254 256 return None
255 257
256 258 user.update_lastlogin()
257 259 Session.commit()
258 260
259 261 log.debug('User %s is now logged in by container authentication',
260 262 user.username)
261 263 return user
262 264
265
263 266 def get_container_username(environ, config):
264 267 username = None
265 268
266 269 if str2bool(config.get('container_auth_enabled', False)):
267 270 from paste.httpheaders import REMOTE_USER
268 271 username = REMOTE_USER(environ)
269 272
270 273 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
271 274 username = environ.get('HTTP_X_FORWARDED_USER')
272 275
273 276 if username:
274 277 # Removing realm and domain from username
275 278 username = username.partition('@')[0]
276 279 username = username.rpartition('\\')[2]
277 280 log.debug('Received username %s from container', username)
278 281
279 282 return username
280 283
284
281 285 class AuthUser(object):
282 286 """
283 287 A simple object that handles all attributes of user in RhodeCode
284 288
285 289 It does lookup based on API key,given user, or user present in session
286 290 Then it fills all required information for such user. It also checks if
287 291 anonymous access is enabled and if so, it returns default user as logged
288 292 in
289 293 """
290 294
291 295 def __init__(self, user_id=None, api_key=None, username=None):
292 296
293 297 self.user_id = user_id
294 298 self.api_key = None
295 299 self.username = username
296 300
297 301 self.name = ''
298 302 self.lastname = ''
299 303 self.email = ''
300 304 self.is_authenticated = False
301 305 self.admin = False
302 306 self.permissions = {}
303 307 self._api_key = api_key
304 308 self.propagate_data()
309 self._instance = None
305 310
306 311 def propagate_data(self):
307 312 user_model = UserModel()
308 313 self.anonymous_user = User.get_by_username('default', cache=True)
309 314 is_user_loaded = False
310 315
311 316 # try go get user by api key
312 317 if self._api_key and self._api_key != self.anonymous_user.api_key:
313 318 log.debug('Auth User lookup by API KEY %s', self._api_key)
314 319 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
315 320 # lookup by userid
316 321 elif (self.user_id is not None and
317 322 self.user_id != self.anonymous_user.user_id):
318 323 log.debug('Auth User lookup by USER ID %s', self.user_id)
319 324 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
320 325 # lookup by username
321 326 elif self.username and \
322 327 str2bool(config.get('container_auth_enabled', False)):
323 328
324 329 log.debug('Auth User lookup by USER NAME %s', self.username)
325 330 dbuser = login_container_auth(self.username)
326 331 if dbuser is not None:
327 332 for k, v in dbuser.get_dict().items():
328 333 setattr(self, k, v)
329 334 self.set_authenticated()
330 335 is_user_loaded = True
331 336
332 337 if not is_user_loaded:
333 338 # if we cannot authenticate user try anonymous
334 339 if self.anonymous_user.active is True:
335 340 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
336 341 # then we set this user is logged in
337 342 self.is_authenticated = True
338 343 else:
339 344 self.user_id = None
340 345 self.username = None
341 346 self.is_authenticated = False
342 347
343 348 if not self.username:
344 349 self.username = 'None'
345 350
346 351 log.debug('Auth User is now %s', self)
347 352 user_model.fill_perms(self)
348 353
349 354 @property
350 355 def is_admin(self):
351 356 return self.admin
352 357
353 @property
354 def full_contact(self):
355 return '%s %s <%s>' % (self.name, self.lastname, self.email)
356
357 358 def __repr__(self):
358 359 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
359 360 self.is_authenticated)
360 361
361 362 def set_authenticated(self, authenticated=True):
362 363 if self.user_id != self.anonymous_user.user_id:
363 364 self.is_authenticated = authenticated
364 365
365 366 def get_cookie_store(self):
366 return {'username':self.username,
367 return {'username': self.username,
367 368 'user_id': self.user_id,
368 'is_authenticated':self.is_authenticated}
369 'is_authenticated': self.is_authenticated}
369 370
370 371 @classmethod
371 372 def from_cookie_store(cls, cookie_store):
372 373 user_id = cookie_store.get('user_id')
373 374 username = cookie_store.get('username')
374 375 api_key = cookie_store.get('api_key')
375 376 return AuthUser(user_id, api_key, username)
376 377
378
377 379 def set_available_permissions(config):
378 380 """
379 381 This function will propagate pylons globals with all available defined
380 382 permission given in db. We don't want to check each time from db for new
381 383 permissions since adding a new permission also requires application restart
382 384 ie. to decorate new views with the newly created permission
383 385
384 386 :param config: current pylons config instance
385 387
386 388 """
387 389 log.info('getting information about all available permissions')
388 390 try:
389 391 sa = meta.Session
390 392 all_perms = sa.query(Permission).all()
391 except:
393 except Exception:
392 394 pass
393 395 finally:
394 396 meta.Session.remove()
395 397
396 398 config['available_permissions'] = [x.permission_name for x in all_perms]
397 399
398 400
399 401 #==============================================================================
400 402 # CHECK DECORATORS
401 403 #==============================================================================
402 404 class LoginRequired(object):
403 405 """
404 406 Must be logged in to execute this function else
405 407 redirect to login page
406 408
407 409 :param api_access: if enabled this checks only for valid auth token
408 410 and grants access based on valid token
409 411 """
410 412
411 413 def __init__(self, api_access=False):
412 414 self.api_access = api_access
413 415
414 416 def __call__(self, func):
415 417 return decorator(self.__wrapper, func)
416 418
417 419 def __wrapper(self, func, *fargs, **fkwargs):
418 420 cls = fargs[0]
419 421 user = cls.rhodecode_user
420 422
421 423 api_access_ok = False
422 424 if self.api_access:
423 425 log.debug('Checking API KEY access for %s', cls)
424 426 if user.api_key == request.GET.get('api_key'):
425 427 api_access_ok = True
426 428 else:
427 429 log.debug("API KEY token not valid")
428 430
429 431 log.debug('Checking if %s is authenticated @ %s', user.username, cls)
430 432 if user.is_authenticated or api_access_ok:
431 433 log.debug('user %s is authenticated', user.username)
432 434 return func(*fargs, **fkwargs)
433 435 else:
434 436 log.warn('user %s NOT authenticated', user.username)
435 437 p = url.current()
436 438
437 439 log.debug('redirecting to login page with %s', p)
438 440 return redirect(url('login_home', came_from=p))
439 441
440 442
441 443 class NotAnonymous(object):
442 444 """
443 445 Must be logged in to execute this function else
444 446 redirect to login page"""
445 447
446 448 def __call__(self, func):
447 449 return decorator(self.__wrapper, func)
448 450
449 451 def __wrapper(self, func, *fargs, **fkwargs):
450 452 cls = fargs[0]
451 453 self.user = cls.rhodecode_user
452 454
453 455 log.debug('Checking if user is not anonymous @%s', cls)
454 456
455 457 anonymous = self.user.username == 'default'
456 458
457 459 if anonymous:
458 460 p = url.current()
459 461
460 462 import rhodecode.lib.helpers as h
461 463 h.flash(_('You need to be a registered user to '
462 464 'perform this action'),
463 465 category='warning')
464 466 return redirect(url('login_home', came_from=p))
465 467 else:
466 468 return func(*fargs, **fkwargs)
467 469
468 470
469 471 class PermsDecorator(object):
470 472 """Base class for controller decorators"""
471 473
472 474 def __init__(self, *required_perms):
473 475 available_perms = config['available_permissions']
474 476 for perm in required_perms:
475 477 if perm not in available_perms:
476 478 raise Exception("'%s' permission is not defined" % perm)
477 479 self.required_perms = set(required_perms)
478 480 self.user_perms = None
479 481
480 482 def __call__(self, func):
481 483 return decorator(self.__wrapper, func)
482 484
483 485 def __wrapper(self, func, *fargs, **fkwargs):
484 486 cls = fargs[0]
485 487 self.user = cls.rhodecode_user
486 488 self.user_perms = self.user.permissions
487 489 log.debug('checking %s permissions %s for %s %s',
488 490 self.__class__.__name__, self.required_perms, cls,
489 491 self.user)
490 492
491 493 if self.check_permissions():
492 494 log.debug('Permission granted for %s %s', cls, self.user)
493 495 return func(*fargs, **fkwargs)
494 496
495 497 else:
496 498 log.warning('Permission denied for %s %s', cls, self.user)
497 499 anonymous = self.user.username == 'default'
498 500
499 501 if anonymous:
500 502 p = url.current()
501 503
502 504 import rhodecode.lib.helpers as h
503 505 h.flash(_('You need to be a signed in to '
504 506 'view this page'),
505 507 category='warning')
506 508 return redirect(url('login_home', came_from=p))
507 509
508 510 else:
509 511 # redirect with forbidden ret code
510 512 return abort(403)
511 513
512 514 def check_permissions(self):
513 515 """Dummy function for overriding"""
514 516 raise Exception('You have to write this function in child class')
515 517
516 518
517 519 class HasPermissionAllDecorator(PermsDecorator):
518 520 """
519 521 Checks for access permission for all given predicates. All of them
520 522 have to be meet in order to fulfill the request
521 523 """
522 524
523 525 def check_permissions(self):
524 526 if self.required_perms.issubset(self.user_perms.get('global')):
525 527 return True
526 528 return False
527 529
528 530
529 531 class HasPermissionAnyDecorator(PermsDecorator):
530 532 """
531 533 Checks for access permission for any of given predicates. In order to
532 534 fulfill the request any of predicates must be meet
533 535 """
534 536
535 537 def check_permissions(self):
536 538 if self.required_perms.intersection(self.user_perms.get('global')):
537 539 return True
538 540 return False
539 541
540 542
541 543 class HasRepoPermissionAllDecorator(PermsDecorator):
542 544 """
543 545 Checks for access permission for all given predicates for specific
544 546 repository. All of them have to be meet in order to fulfill the request
545 547 """
546 548
547 549 def check_permissions(self):
548 550 repo_name = get_repo_slug(request)
549 551 try:
550 552 user_perms = set([self.user_perms['repositories'][repo_name]])
551 553 except KeyError:
552 554 return False
553 555 if self.required_perms.issubset(user_perms):
554 556 return True
555 557 return False
556 558
557 559
558 560 class HasRepoPermissionAnyDecorator(PermsDecorator):
559 561 """
560 562 Checks for access permission for any of given predicates for specific
561 563 repository. In order to fulfill the request any of predicates must be meet
562 564 """
563 565
564 566 def check_permissions(self):
565 567 repo_name = get_repo_slug(request)
566 568
567 569 try:
568 570 user_perms = set([self.user_perms['repositories'][repo_name]])
569 571 except KeyError:
570 572 return False
571 573 if self.required_perms.intersection(user_perms):
572 574 return True
573 575 return False
574 576
575 577
576 578 #==============================================================================
577 579 # CHECK FUNCTIONS
578 580 #==============================================================================
579 581 class PermsFunction(object):
580 582 """Base function for other check functions"""
581 583
582 584 def __init__(self, *perms):
583 585 available_perms = config['available_permissions']
584 586
585 587 for perm in perms:
586 588 if perm not in available_perms:
587 589 raise Exception("'%s' permission in not defined" % perm)
588 590 self.required_perms = set(perms)
589 591 self.user_perms = None
590 592 self.granted_for = ''
591 593 self.repo_name = None
592 594
593 595 def __call__(self, check_Location=''):
594 596 user = request.user
595 597 if not user:
596 598 return False
597 599 self.user_perms = user.permissions
598 600 self.granted_for = user
599 601 log.debug('checking %s %s %s', self.__class__.__name__,
600 602 self.required_perms, user)
601 603
602 604 if self.check_permissions():
603 605 log.debug('Permission granted %s @ %s', self.granted_for,
604 606 check_Location or 'unspecified location')
605 607 return True
606 608
607 609 else:
608 610 log.warning('Permission denied for %s @ %s', self.granted_for,
609 611 check_Location or 'unspecified location')
610 612 return False
611 613
612 614 def check_permissions(self):
613 615 """Dummy function for overriding"""
614 616 raise Exception('You have to write this function in child class')
615 617
616 618
617 619 class HasPermissionAll(PermsFunction):
618 620 def check_permissions(self):
619 621 if self.required_perms.issubset(self.user_perms.get('global')):
620 622 return True
621 623 return False
622 624
623 625
624 626 class HasPermissionAny(PermsFunction):
625 627 def check_permissions(self):
626 628 if self.required_perms.intersection(self.user_perms.get('global')):
627 629 return True
628 630 return False
629 631
630 632
631 633 class HasRepoPermissionAll(PermsFunction):
632 634
633 635 def __call__(self, repo_name=None, check_Location=''):
634 636 self.repo_name = repo_name
635 637 return super(HasRepoPermissionAll, self).__call__(check_Location)
636 638
637 639 def check_permissions(self):
638 640 if not self.repo_name:
639 641 self.repo_name = get_repo_slug(request)
640 642
641 643 try:
642 644 self.user_perms = set([self.user_perms['reposit'
643 645 'ories'][self.repo_name]])
644 646 except KeyError:
645 647 return False
646 648 self.granted_for = self.repo_name
647 649 if self.required_perms.issubset(self.user_perms):
648 650 return True
649 651 return False
650 652
651 653
652 654 class HasRepoPermissionAny(PermsFunction):
653 655
654 656 def __call__(self, repo_name=None, check_Location=''):
655 657 self.repo_name = repo_name
656 658 return super(HasRepoPermissionAny, self).__call__(check_Location)
657 659
658 660 def check_permissions(self):
659 661 if not self.repo_name:
660 662 self.repo_name = get_repo_slug(request)
661 663
662 664 try:
663 665 self.user_perms = set([self.user_perms['reposi'
664 666 'tories'][self.repo_name]])
665 667 except KeyError:
666 668 return False
667 669 self.granted_for = self.repo_name
668 670 if self.required_perms.intersection(self.user_perms):
669 671 return True
670 672 return False
671 673
672 674
673 675 #==============================================================================
674 676 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
675 677 #==============================================================================
676 678 class HasPermissionAnyMiddleware(object):
677 679 def __init__(self, *perms):
678 680 self.required_perms = set(perms)
679 681
680 682 def __call__(self, user, repo_name):
681 683 usr = AuthUser(user.user_id)
682 684 try:
683 685 self.user_perms = set([usr.permissions['repositories'][repo_name]])
684 686 except:
685 687 self.user_perms = set()
686 688 self.granted_for = ''
687 689 self.username = user.username
688 690 self.repo_name = repo_name
689 691 return self.check_permissions()
690 692
691 693 def check_permissions(self):
692 694 log.debug('checking mercurial protocol '
693 695 'permissions %s for user:%s repository:%s', self.user_perms,
694 696 self.username, self.repo_name)
695 697 if self.required_perms.intersection(self.user_perms):
696 698 log.debug('permission granted')
697 699 return True
698 700 log.debug('permission denied')
699 701 return False
@@ -1,1136 +1,1146 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from collections import defaultdict
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from vcs import get_backend
38 38 from vcs.utils.helpers import get_scm
39 39 from vcs.exceptions import VCSError
40 40 from vcs.utils.lazy import LazyProperty
41 41
42 42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode
43 43 from rhodecode.lib.exceptions import UsersGroupsAssignedException
44 44 from rhodecode.lib.compat import json
45 45 from rhodecode.lib.caching_query import FromCache
46 46
47 47 from rhodecode.model.meta import Base, Session
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 #==============================================================================
52 52 # BASE CLASSES
53 53 #==============================================================================
54 54
55 55
56 56 class ModelSerializer(json.JSONEncoder):
57 57 """
58 58 Simple Serializer for JSON,
59 59
60 60 usage::
61 61
62 62 to make object customized for serialization implement a __json__
63 63 method that will return a dict for serialization into json
64 64
65 65 example::
66 66
67 67 class Task(object):
68 68
69 69 def __init__(self, name, value):
70 70 self.name = name
71 71 self.value = value
72 72
73 73 def __json__(self):
74 74 return dict(name=self.name,
75 75 value=self.value)
76 76
77 77 """
78 78
79 79 def default(self, obj):
80 80
81 81 if hasattr(obj, '__json__'):
82 82 return obj.__json__()
83 83 else:
84 84 return json.JSONEncoder.default(self, obj)
85 85
86 86
87 87 class BaseModel(object):
88 88 """
89 89 Base Model for all classess
90 90 """
91 91
92 92 @classmethod
93 93 def _get_keys(cls):
94 94 """return column names for this model """
95 95 return class_mapper(cls).c.keys()
96 96
97 97 def get_dict(self):
98 98 """
99 99 return dict with keys and values corresponding
100 100 to this model data """
101 101
102 102 d = {}
103 103 for k in self._get_keys():
104 104 d[k] = getattr(self, k)
105 105
106 106 # also use __json__() if present to get additional fields
107 107 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
108 108 d[k] = val
109 109 return d
110 110
111 111 def get_appstruct(self):
112 112 """return list with keys and values tupples corresponding
113 113 to this model data """
114 114
115 115 l = []
116 116 for k in self._get_keys():
117 117 l.append((k, getattr(self, k),))
118 118 return l
119 119
120 120 def populate_obj(self, populate_dict):
121 121 """populate model with data from given populate_dict"""
122 122
123 123 for k in self._get_keys():
124 124 if k in populate_dict:
125 125 setattr(self, k, populate_dict[k])
126 126
127 127 @classmethod
128 128 def query(cls):
129 129 return Session.query(cls)
130 130
131 131 @classmethod
132 132 def get(cls, id_):
133 133 if id_:
134 134 return cls.query().get(id_)
135 135
136 136 @classmethod
137 137 def getAll(cls):
138 138 return cls.query().all()
139 139
140 140 @classmethod
141 141 def delete(cls, id_):
142 142 obj = cls.query().get(id_)
143 143 Session.delete(obj)
144 144
145 145
146 146 class RhodeCodeSetting(Base, BaseModel):
147 147 __tablename__ = 'rhodecode_settings'
148 148 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing': True})
149 149 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
150 150 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
151 151 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
152 152
153 153 def __init__(self, k='', v=''):
154 154 self.app_settings_name = k
155 155 self.app_settings_value = v
156 156
157 157
158 158 @validates('_app_settings_value')
159 159 def validate_settings_value(self, key, val):
160 160 assert type(val) == unicode
161 161 return val
162 162
163 163 @hybrid_property
164 164 def app_settings_value(self):
165 165 v = self._app_settings_value
166 166 if v == 'ldap_active':
167 167 v = str2bool(v)
168 168 return v
169 169
170 170 @app_settings_value.setter
171 171 def app_settings_value(self, val):
172 172 """
173 173 Setter that will always make sure we use unicode in app_settings_value
174 174
175 175 :param val:
176 176 """
177 177 self._app_settings_value = safe_unicode(val)
178 178
179 179 def __repr__(self):
180 180 return "<%s('%s:%s')>" % (self.__class__.__name__,
181 181 self.app_settings_name, self.app_settings_value)
182 182
183 183 @classmethod
184 184 def get_by_name(cls, ldap_key):
185 185 return cls.query()\
186 186 .filter(cls.app_settings_name == ldap_key).scalar()
187 187
188 188 @classmethod
189 189 def get_app_settings(cls, cache=False):
190 190
191 191 ret = cls.query()
192 192
193 193 if cache:
194 194 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
195 195
196 196 if not ret:
197 197 raise Exception('Could not get application settings !')
198 198 settings = {}
199 199 for each in ret:
200 200 settings['rhodecode_' + each.app_settings_name] = \
201 201 each.app_settings_value
202 202
203 203 return settings
204 204
205 205 @classmethod
206 206 def get_ldap_settings(cls, cache=False):
207 207 ret = cls.query()\
208 208 .filter(cls.app_settings_name.startswith('ldap_')).all()
209 209 fd = {}
210 210 for row in ret:
211 211 fd.update({row.app_settings_name:row.app_settings_value})
212 212
213 213 return fd
214 214
215 215
216 216 class RhodeCodeUi(Base, BaseModel):
217 217 __tablename__ = 'rhodecode_ui'
218 218 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing': True})
219 219
220 220 HOOK_UPDATE = 'changegroup.update'
221 221 HOOK_REPO_SIZE = 'changegroup.repo_size'
222 222 HOOK_PUSH = 'pretxnchangegroup.push_logger'
223 223 HOOK_PULL = 'preoutgoing.pull_logger'
224 224
225 225 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
226 226 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
227 227 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
228 228 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
229 229 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
230 230
231 231 @classmethod
232 232 def get_by_key(cls, key):
233 233 return cls.query().filter(cls.ui_key == key)
234 234
235 235 @classmethod
236 236 def get_builtin_hooks(cls):
237 237 q = cls.query()
238 238 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
239 239 cls.HOOK_REPO_SIZE,
240 240 cls.HOOK_PUSH, cls.HOOK_PULL]))
241 241 return q.all()
242 242
243 243 @classmethod
244 244 def get_custom_hooks(cls):
245 245 q = cls.query()
246 246 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
247 247 cls.HOOK_REPO_SIZE,
248 248 cls.HOOK_PUSH, cls.HOOK_PULL]))
249 249 q = q.filter(cls.ui_section == 'hooks')
250 250 return q.all()
251 251
252 252 @classmethod
253 253 def create_or_update_hook(cls, key, val):
254 254 new_ui = cls.get_by_key(key).scalar() or cls()
255 255 new_ui.ui_section = 'hooks'
256 256 new_ui.ui_active = True
257 257 new_ui.ui_key = key
258 258 new_ui.ui_value = val
259 259
260 260 Session.add(new_ui)
261 261
262 262
263 263 class User(Base, BaseModel):
264 264 __tablename__ = 'users'
265 265 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing': True})
266 266 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
267 267 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
268 268 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 269 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
270 270 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
271 271 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
272 272 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
273 273 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
274 274 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
275 275 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
276 276 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
277 277
278 278 user_log = relationship('UserLog', cascade='all')
279 279 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
280 280
281 281 repositories = relationship('Repository')
282 282 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
283 283 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
284 284
285 285 group_member = relationship('UsersGroupMember', cascade='all')
286 286
287 287 notifications = relationship('UserNotification',)
288 288
289 289 @hybrid_property
290 290 def email(self):
291 291 return self._email
292 292
293 293 @email.setter
294 294 def email(self, val):
295 295 self._email = val.lower() if val else None
296 296
297 297 @property
298 298 def full_name(self):
299 299 return '%s %s' % (self.name, self.lastname)
300 300
301 301 @property
302 def full_name_or_username(self):
303 return ('%s %s' % (self.name, self.lastname)
304 if (self.name and self.lastname) else self.username)
305
306 @property
302 307 def full_contact(self):
303 308 return '%s %s <%s>' % (self.name, self.lastname, self.email)
304 309
305 310 @property
306 311 def short_contact(self):
307 312 return '%s %s' % (self.name, self.lastname)
308 313
309 314 @property
310 315 def is_admin(self):
311 316 return self.admin
312 317
313 318 def __repr__(self):
314 319 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
315 320 self.user_id, self.username)
316 321
317 322 @classmethod
318 323 def get_by_username(cls, username, case_insensitive=False, cache=False):
319 324 if case_insensitive:
320 325 q = cls.query().filter(cls.username.ilike(username))
321 326 else:
322 327 q = cls.query().filter(cls.username == username)
323 328
324 329 if cache:
325 330 q = q.options(FromCache("sql_cache_short",
326 331 "get_user_%s" % username))
327 332 return q.scalar()
328 333
329 334 @classmethod
330 335 def get_by_api_key(cls, api_key, cache=False):
331 336 q = cls.query().filter(cls.api_key == api_key)
332 337
333 338 if cache:
334 339 q = q.options(FromCache("sql_cache_short",
335 340 "get_api_key_%s" % api_key))
336 341 return q.scalar()
337 342
338 343 @classmethod
339 344 def get_by_email(cls, email, case_insensitive=False, cache=False):
340 345 if case_insensitive:
341 346 q = cls.query().filter(cls.email.ilike(email))
342 347 else:
343 348 q = cls.query().filter(cls.email == email)
344 349
345 350 if cache:
346 351 q = q.options(FromCache("sql_cache_short",
347 352 "get_api_key_%s" % email))
348 353 return q.scalar()
349 354
350 355 def update_lastlogin(self):
351 356 """Update user lastlogin"""
352 357 self.last_login = datetime.datetime.now()
353 358 Session.add(self)
354 359 log.debug('updated user %s lastlogin', self.username)
355 360
356 361 def __json__(self):
357 return dict(email=self.email,
358 full_name=self.full_name)
362 return dict(
363 email=self.email,
364 full_name=self.full_name,
365 full_name_or_username=self.full_name_or_username,
366 short_contact=self.short_contact,
367 full_contact=self.full_contact
368 )
359 369
360 370
361 371 class UserLog(Base, BaseModel):
362 372 __tablename__ = 'user_logs'
363 373 __table_args__ = {'extend_existing': True}
364 374 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
365 375 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
366 376 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
367 377 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
368 378 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
369 379 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
370 380 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
371 381
372 382 @property
373 383 def action_as_day(self):
374 384 return datetime.date(*self.action_date.timetuple()[:3])
375 385
376 386 user = relationship('User')
377 387 repository = relationship('Repository',cascade='')
378 388
379 389
380 390 class UsersGroup(Base, BaseModel):
381 391 __tablename__ = 'users_groups'
382 392 __table_args__ = {'extend_existing': True}
383 393
384 394 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
385 395 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
386 396 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
387 397
388 398 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
389 399
390 400 def __repr__(self):
391 401 return '<userGroup(%s)>' % (self.users_group_name)
392 402
393 403 @classmethod
394 404 def get_by_group_name(cls, group_name, cache=False,
395 405 case_insensitive=False):
396 406 if case_insensitive:
397 407 q = cls.query().filter(cls.users_group_name.ilike(group_name))
398 408 else:
399 409 q = cls.query().filter(cls.users_group_name == group_name)
400 410 if cache:
401 411 q = q.options(FromCache("sql_cache_short",
402 412 "get_user_%s" % group_name))
403 413 return q.scalar()
404 414
405 415 @classmethod
406 416 def get(cls, users_group_id, cache=False):
407 417 users_group = cls.query()
408 418 if cache:
409 419 users_group = users_group.options(FromCache("sql_cache_short",
410 420 "get_users_group_%s" % users_group_id))
411 421 return users_group.get(users_group_id)
412 422
413 423
414 424 class UsersGroupMember(Base, BaseModel):
415 425 __tablename__ = 'users_groups_members'
416 426 __table_args__ = {'extend_existing': True}
417 427
418 428 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
419 429 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
420 430 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
421 431
422 432 user = relationship('User', lazy='joined')
423 433 users_group = relationship('UsersGroup')
424 434
425 435 def __init__(self, gr_id='', u_id=''):
426 436 self.users_group_id = gr_id
427 437 self.user_id = u_id
428 438
429 439 @staticmethod
430 440 def add_user_to_group(group, user):
431 441 ugm = UsersGroupMember()
432 442 ugm.users_group = group
433 443 ugm.user = user
434 444 Session.add(ugm)
435 445 Session.commit()
436 446 return ugm
437 447
438 448
439 449 class Repository(Base, BaseModel):
440 450 __tablename__ = 'repositories'
441 451 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing': True},)
442 452
443 453 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
444 454 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
445 455 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
446 456 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
447 457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
448 458 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
449 459 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
450 460 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
451 461 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
452 462 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
453 463
454 464 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
455 465 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
456 466
457 467 user = relationship('User')
458 468 fork = relationship('Repository', remote_side=repo_id)
459 469 group = relationship('RepoGroup')
460 470 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
461 471 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
462 472 stats = relationship('Statistics', cascade='all', uselist=False)
463 473
464 474 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
465 475
466 476 logs = relationship('UserLog')
467 477
468 478 def __repr__(self):
469 479 return "<%s('%s:%s')>" % (self.__class__.__name__,
470 480 self.repo_id, self.repo_name)
471 481
472 482 @classmethod
473 483 def url_sep(cls):
474 484 return '/'
475 485
476 486 @classmethod
477 487 def get_by_repo_name(cls, repo_name):
478 488 q = Session.query(cls).filter(cls.repo_name == repo_name)
479 489 q = q.options(joinedload(Repository.fork))\
480 490 .options(joinedload(Repository.user))\
481 491 .options(joinedload(Repository.group))
482 492 return q.scalar()
483 493
484 494 @classmethod
485 495 def get_repo_forks(cls, repo_id):
486 496 return cls.query().filter(Repository.fork_id == repo_id)
487 497
488 498 @classmethod
489 499 def base_path(cls):
490 500 """
491 501 Returns base path when all repos are stored
492 502
493 503 :param cls:
494 504 """
495 505 q = Session.query(RhodeCodeUi)\
496 506 .filter(RhodeCodeUi.ui_key == cls.url_sep())
497 507 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
498 508 return q.one().ui_value
499 509
500 510 @property
501 511 def just_name(self):
502 512 return self.repo_name.split(Repository.url_sep())[-1]
503 513
504 514 @property
505 515 def groups_with_parents(self):
506 516 groups = []
507 517 if self.group is None:
508 518 return groups
509 519
510 520 cur_gr = self.group
511 521 groups.insert(0, cur_gr)
512 522 while 1:
513 523 gr = getattr(cur_gr, 'parent_group', None)
514 524 cur_gr = cur_gr.parent_group
515 525 if gr is None:
516 526 break
517 527 groups.insert(0, gr)
518 528
519 529 return groups
520 530
521 531 @property
522 532 def groups_and_repo(self):
523 533 return self.groups_with_parents, self.just_name
524 534
525 535 @LazyProperty
526 536 def repo_path(self):
527 537 """
528 538 Returns base full path for that repository means where it actually
529 539 exists on a filesystem
530 540 """
531 541 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
532 542 Repository.url_sep())
533 543 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
534 544 return q.one().ui_value
535 545
536 546 @property
537 547 def repo_full_path(self):
538 548 p = [self.repo_path]
539 549 # we need to split the name by / since this is how we store the
540 550 # names in the database, but that eventually needs to be converted
541 551 # into a valid system path
542 552 p += self.repo_name.split(Repository.url_sep())
543 553 return os.path.join(*p)
544 554
545 555 def get_new_name(self, repo_name):
546 556 """
547 557 returns new full repository name based on assigned group and new new
548 558
549 559 :param group_name:
550 560 """
551 561 path_prefix = self.group.full_path_splitted if self.group else []
552 562 return Repository.url_sep().join(path_prefix + [repo_name])
553 563
554 564 @property
555 565 def _ui(self):
556 566 """
557 567 Creates an db based ui object for this repository
558 568 """
559 569 from mercurial import ui
560 570 from mercurial import config
561 571 baseui = ui.ui()
562 572
563 573 #clean the baseui object
564 574 baseui._ocfg = config.config()
565 575 baseui._ucfg = config.config()
566 576 baseui._tcfg = config.config()
567 577
568 578 ret = RhodeCodeUi.query()\
569 579 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
570 580
571 581 hg_ui = ret
572 582 for ui_ in hg_ui:
573 583 if ui_.ui_active:
574 584 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
575 585 ui_.ui_key, ui_.ui_value)
576 586 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
577 587
578 588 return baseui
579 589
580 590 @classmethod
581 591 def is_valid(cls, repo_name):
582 592 """
583 593 returns True if given repo name is a valid filesystem repository
584 594
585 595 :param cls:
586 596 :param repo_name:
587 597 """
588 598 from rhodecode.lib.utils import is_valid_repo
589 599
590 600 return is_valid_repo(repo_name, cls.base_path())
591 601
592 602 #==========================================================================
593 603 # SCM PROPERTIES
594 604 #==========================================================================
595 605
596 606 def get_changeset(self, rev):
597 607 return get_changeset_safe(self.scm_instance, rev)
598 608
599 609 @property
600 610 def tip(self):
601 611 return self.get_changeset('tip')
602 612
603 613 @property
604 614 def author(self):
605 615 return self.tip.author
606 616
607 617 @property
608 618 def last_change(self):
609 619 return self.scm_instance.last_change
610 620
611 621 def comments(self, revisions=None):
612 622 """
613 623 Returns comments for this repository grouped by revisions
614 624
615 625 :param revisions: filter query by revisions only
616 626 """
617 627 cmts = ChangesetComment.query()\
618 628 .filter(ChangesetComment.repo == self)
619 629 if revisions:
620 630 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
621 631 grouped = defaultdict(list)
622 632 for cmt in cmts.all():
623 633 grouped[cmt.revision].append(cmt)
624 634 return grouped
625 635
626 636
627 637 #==========================================================================
628 638 # SCM CACHE INSTANCE
629 639 #==========================================================================
630 640
631 641 @property
632 642 def invalidate(self):
633 643 return CacheInvalidation.invalidate(self.repo_name)
634 644
635 645 def set_invalidate(self):
636 646 """
637 647 set a cache for invalidation for this instance
638 648 """
639 649 CacheInvalidation.set_invalidate(self.repo_name)
640 650
641 651 @LazyProperty
642 652 def scm_instance(self):
643 653 return self.__get_instance()
644 654
645 655 @property
646 656 def scm_instance_cached(self):
647 657 @cache_region('long_term')
648 658 def _c(repo_name):
649 659 return self.__get_instance()
650 660 rn = self.repo_name
651 661 log.debug('Getting cached instance of repo')
652 662 inv = self.invalidate
653 663 if inv is not None:
654 664 region_invalidate(_c, None, rn)
655 665 # update our cache
656 666 CacheInvalidation.set_valid(inv.cache_key)
657 667 return _c(rn)
658 668
659 669 def __get_instance(self):
660 670 repo_full_path = self.repo_full_path
661 671 try:
662 672 alias = get_scm(repo_full_path)[0]
663 673 log.debug('Creating instance of %s repository', alias)
664 674 backend = get_backend(alias)
665 675 except VCSError:
666 676 log.error(traceback.format_exc())
667 677 log.error('Perhaps this repository is in db and not in '
668 678 'filesystem run rescan repositories with '
669 679 '"destroy old data " option from admin panel')
670 680 return
671 681
672 682 if alias == 'hg':
673 683 repo = backend(safe_str(repo_full_path), create=False,
674 684 baseui=self._ui)
675 685 # skip hidden web repository
676 686 if repo._get_hidden():
677 687 return
678 688 else:
679 689 repo = backend(repo_full_path, create=False)
680 690
681 691 return repo
682 692
683 693
684 694 class RepoGroup(Base, BaseModel):
685 695 __tablename__ = 'groups'
686 696 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
687 697 CheckConstraint('group_id != group_parent_id'), {'extend_existing': True},)
688 698 __mapper_args__ = {'order_by':'group_name'}
689 699
690 700 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
691 701 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
692 702 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
693 703 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
694 704
695 705 parent_group = relationship('RepoGroup', remote_side=group_id)
696 706
697 707 def __init__(self, group_name='', parent_group=None):
698 708 self.group_name = group_name
699 709 self.parent_group = parent_group
700 710
701 711 def __repr__(self):
702 712 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
703 713 self.group_name)
704 714
705 715 @classmethod
706 716 def groups_choices(cls):
707 717 from webhelpers.html import literal as _literal
708 718 repo_groups = [('', '')]
709 719 sep = ' &raquo; '
710 720 _name = lambda k: _literal(sep.join(k))
711 721
712 722 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
713 723 for x in cls.query().all()])
714 724
715 725 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
716 726 return repo_groups
717 727
718 728 @classmethod
719 729 def url_sep(cls):
720 730 return '/'
721 731
722 732 @classmethod
723 733 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
724 734 if case_insensitive:
725 735 gr = cls.query()\
726 736 .filter(cls.group_name.ilike(group_name))
727 737 else:
728 738 gr = cls.query()\
729 739 .filter(cls.group_name == group_name)
730 740 if cache:
731 741 gr = gr.options(FromCache("sql_cache_short",
732 742 "get_group_%s" % group_name))
733 743 return gr.scalar()
734 744
735 745 @property
736 746 def parents(self):
737 747 parents_recursion_limit = 5
738 748 groups = []
739 749 if self.parent_group is None:
740 750 return groups
741 751 cur_gr = self.parent_group
742 752 groups.insert(0, cur_gr)
743 753 cnt = 0
744 754 while 1:
745 755 cnt += 1
746 756 gr = getattr(cur_gr, 'parent_group', None)
747 757 cur_gr = cur_gr.parent_group
748 758 if gr is None:
749 759 break
750 760 if cnt == parents_recursion_limit:
751 761 # this will prevent accidental infinit loops
752 762 log.error('group nested more than %s' %
753 763 parents_recursion_limit)
754 764 break
755 765
756 766 groups.insert(0, gr)
757 767 return groups
758 768
759 769 @property
760 770 def children(self):
761 771 return RepoGroup.query().filter(RepoGroup.parent_group == self)
762 772
763 773 @property
764 774 def name(self):
765 775 return self.group_name.split(RepoGroup.url_sep())[-1]
766 776
767 777 @property
768 778 def full_path(self):
769 779 return self.group_name
770 780
771 781 @property
772 782 def full_path_splitted(self):
773 783 return self.group_name.split(RepoGroup.url_sep())
774 784
775 785 @property
776 786 def repositories(self):
777 787 return Repository.query().filter(Repository.group == self)
778 788
779 789 @property
780 790 def repositories_recursive_count(self):
781 791 cnt = self.repositories.count()
782 792
783 793 def children_count(group):
784 794 cnt = 0
785 795 for child in group.children:
786 796 cnt += child.repositories.count()
787 797 cnt += children_count(child)
788 798 return cnt
789 799
790 800 return cnt + children_count(self)
791 801
792 802
793 803 def get_new_name(self, group_name):
794 804 """
795 805 returns new full group name based on parent and new name
796 806
797 807 :param group_name:
798 808 """
799 809 path_prefix = (self.parent_group.full_path_splitted if
800 810 self.parent_group else [])
801 811 return RepoGroup.url_sep().join(path_prefix + [group_name])
802 812
803 813
804 814 class Permission(Base, BaseModel):
805 815 __tablename__ = 'permissions'
806 816 __table_args__ = {'extend_existing': True}
807 817 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
808 818 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
809 819 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
810 820
811 821 def __repr__(self):
812 822 return "<%s('%s:%s')>" % (self.__class__.__name__,
813 823 self.permission_id, self.permission_name)
814 824
815 825 @classmethod
816 826 def get_by_key(cls, key):
817 827 return cls.query().filter(cls.permission_name == key).scalar()
818 828
819 829 @classmethod
820 830 def get_default_perms(cls, default_user_id):
821 831 q = Session.query(UserRepoToPerm, Repository, cls)\
822 832 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
823 833 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
824 834 .filter(UserRepoToPerm.user_id == default_user_id)
825 835
826 836 return q.all()
827 837
828 838
829 839 class UserRepoToPerm(Base, BaseModel):
830 840 __tablename__ = 'repo_to_perm'
831 841 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing': True})
832 842 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
833 843 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
834 844 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
835 845 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
836 846
837 847 user = relationship('User')
838 848 permission = relationship('Permission')
839 849 repository = relationship('Repository')
840 850
841 851 @classmethod
842 852 def create(cls, user, repository, permission):
843 853 n = cls()
844 854 n.user = user
845 855 n.repository = repository
846 856 n.permission = permission
847 857 Session.add(n)
848 858 return n
849 859
850 860 def __repr__(self):
851 861 return '<user:%s => %s >' % (self.user, self.repository)
852 862
853 863
854 864 class UserToPerm(Base, BaseModel):
855 865 __tablename__ = 'user_to_perm'
856 866 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing': True})
857 867 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
858 868 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
859 869 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
860 870
861 871 user = relationship('User')
862 872 permission = relationship('Permission', lazy='joined')
863 873
864 874
865 875 class UsersGroupRepoToPerm(Base, BaseModel):
866 876 __tablename__ = 'users_group_repo_to_perm'
867 877 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing': True})
868 878 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
869 879 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
870 880 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
871 881 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
872 882
873 883 users_group = relationship('UsersGroup')
874 884 permission = relationship('Permission')
875 885 repository = relationship('Repository')
876 886
877 887 @classmethod
878 888 def create(cls, users_group, repository, permission):
879 889 n = cls()
880 890 n.users_group = users_group
881 891 n.repository = repository
882 892 n.permission = permission
883 893 Session.add(n)
884 894 return n
885 895
886 896 def __repr__(self):
887 897 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
888 898
889 899
890 900 class UsersGroupToPerm(Base, BaseModel):
891 901 __tablename__ = 'users_group_to_perm'
892 902 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
893 903 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
894 904 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
895 905
896 906 users_group = relationship('UsersGroup')
897 907 permission = relationship('Permission')
898 908
899 909
900 910 class UserRepoGroupToPerm(Base, BaseModel):
901 911 __tablename__ = 'group_to_perm'
902 912 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing': True})
903 913
904 914 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
905 915 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
906 916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
907 917 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
908 918
909 919 user = relationship('User')
910 920 permission = relationship('Permission')
911 921 group = relationship('RepoGroup')
912 922
913 923
914 924 class UsersGroupRepoGroupToPerm(Base, BaseModel):
915 925 __tablename__ = 'users_group_repo_group_to_perm'
916 926 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing': True})
917 927
918 928 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
919 929 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
920 930 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
921 931 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
922 932
923 933 users_group = relationship('UsersGroup')
924 934 permission = relationship('Permission')
925 935 group = relationship('RepoGroup')
926 936
927 937
928 938 class Statistics(Base, BaseModel):
929 939 __tablename__ = 'statistics'
930 940 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing': True})
931 941 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
932 942 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
933 943 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
934 944 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
935 945 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
936 946 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
937 947
938 948 repository = relationship('Repository', single_parent=True)
939 949
940 950
941 951 class UserFollowing(Base, BaseModel):
942 952 __tablename__ = 'user_followings'
943 953 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
944 954 UniqueConstraint('user_id', 'follows_user_id')
945 955 , {'extend_existing': True})
946 956
947 957 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
948 958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
949 959 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
950 960 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
951 961 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
952 962
953 963 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
954 964
955 965 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
956 966 follows_repository = relationship('Repository', order_by='Repository.repo_name')
957 967
958 968
959 969 @classmethod
960 970 def get_repo_followers(cls, repo_id):
961 971 return cls.query().filter(cls.follows_repo_id == repo_id)
962 972
963 973
964 974 class CacheInvalidation(Base, BaseModel):
965 975 __tablename__ = 'cache_invalidation'
966 976 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing': True})
967 977 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
968 978 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
969 979 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
970 980 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
971 981
972 982
973 983 def __init__(self, cache_key, cache_args=''):
974 984 self.cache_key = cache_key
975 985 self.cache_args = cache_args
976 986 self.cache_active = False
977 987
978 988 def __repr__(self):
979 989 return "<%s('%s:%s')>" % (self.__class__.__name__,
980 990 self.cache_id, self.cache_key)
981 991
982 992 @classmethod
983 993 def invalidate(cls, key):
984 994 """
985 995 Returns Invalidation object if this given key should be invalidated
986 996 None otherwise. `cache_active = False` means that this cache
987 997 state is not valid and needs to be invalidated
988 998
989 999 :param key:
990 1000 """
991 1001 return cls.query()\
992 1002 .filter(CacheInvalidation.cache_key == key)\
993 1003 .filter(CacheInvalidation.cache_active == False)\
994 1004 .scalar()
995 1005
996 1006 @classmethod
997 1007 def set_invalidate(cls, key):
998 1008 """
999 1009 Mark this Cache key for invalidation
1000 1010
1001 1011 :param key:
1002 1012 """
1003 1013
1004 1014 log.debug('marking %s for invalidation' % key)
1005 1015 inv_obj = Session.query(cls)\
1006 1016 .filter(cls.cache_key == key).scalar()
1007 1017 if inv_obj:
1008 1018 inv_obj.cache_active = False
1009 1019 else:
1010 1020 log.debug('cache key not found in invalidation db -> creating one')
1011 1021 inv_obj = CacheInvalidation(key)
1012 1022
1013 1023 try:
1014 1024 Session.add(inv_obj)
1015 1025 Session.commit()
1016 1026 except Exception:
1017 1027 log.error(traceback.format_exc())
1018 1028 Session.rollback()
1019 1029
1020 1030 @classmethod
1021 1031 def set_valid(cls, key):
1022 1032 """
1023 1033 Mark this cache key as active and currently cached
1024 1034
1025 1035 :param key:
1026 1036 """
1027 1037 inv_obj = CacheInvalidation.query()\
1028 1038 .filter(CacheInvalidation.cache_key == key).scalar()
1029 1039 inv_obj.cache_active = True
1030 1040 Session.add(inv_obj)
1031 1041 Session.commit()
1032 1042
1033 1043
1034 1044 class ChangesetComment(Base, BaseModel):
1035 1045 __tablename__ = 'changeset_comments'
1036 1046 __table_args__ = ({'extend_existing': True},)
1037 1047 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1038 1048 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1039 1049 revision = Column('revision', String(40), nullable=False)
1040 1050 line_no = Column('line_no', Unicode(10), nullable=True)
1041 1051 f_path = Column('f_path', Unicode(1000), nullable=True)
1042 1052 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1043 1053 text = Column('text', Unicode(25000), nullable=False)
1044 1054 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1045 1055
1046 1056 author = relationship('User', lazy='joined')
1047 1057 repo = relationship('Repository')
1048 1058
1049 1059 @classmethod
1050 1060 def get_users(cls, revision):
1051 1061 """
1052 1062 Returns user associated with this changesetComment. ie those
1053 1063 who actually commented
1054 1064
1055 1065 :param cls:
1056 1066 :param revision:
1057 1067 """
1058 1068 return Session.query(User)\
1059 1069 .filter(cls.revision == revision)\
1060 1070 .join(ChangesetComment.author).all()
1061 1071
1062 1072
1063 1073 class Notification(Base, BaseModel):
1064 1074 __tablename__ = 'notifications'
1065 1075 __table_args__ = ({'extend_existing': True},)
1066 1076
1067 1077 TYPE_CHANGESET_COMMENT = u'cs_comment'
1068 1078 TYPE_MESSAGE = u'message'
1069 1079 TYPE_MENTION = u'mention'
1070 1080 TYPE_REGISTRATION = u'registration'
1071 1081
1072 1082 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1073 1083 subject = Column('subject', Unicode(512), nullable=True)
1074 1084 body = Column('body', Unicode(50000), nullable=True)
1075 1085 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1076 1086 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1077 1087 type_ = Column('type', Unicode(256))
1078 1088
1079 1089 created_by_user = relationship('User')
1080 1090 notifications_to_users = relationship('UserNotification', lazy='joined',
1081 1091 cascade="all, delete, delete-orphan")
1082 1092
1083 1093 @property
1084 1094 def recipients(self):
1085 1095 return [x.user for x in UserNotification.query()\
1086 1096 .filter(UserNotification.notification == self).all()]
1087 1097
1088 1098 @classmethod
1089 1099 def create(cls, created_by, subject, body, recipients, type_=None):
1090 1100 if type_ is None:
1091 1101 type_ = Notification.TYPE_MESSAGE
1092 1102
1093 1103 notification = cls()
1094 1104 notification.created_by_user = created_by
1095 1105 notification.subject = subject
1096 1106 notification.body = body
1097 1107 notification.type_ = type_
1098 1108 notification.created_on = datetime.datetime.now()
1099 1109
1100 1110 for u in recipients:
1101 1111 assoc = UserNotification()
1102 1112 assoc.notification = notification
1103 1113 u.notifications.append(assoc)
1104 1114 Session.add(notification)
1105 1115 return notification
1106 1116
1107 1117 @property
1108 1118 def description(self):
1109 1119 from rhodecode.model.notification import NotificationModel
1110 1120 return NotificationModel().make_description(self)
1111 1121
1112 1122
1113 1123 class UserNotification(Base, BaseModel):
1114 1124 __tablename__ = 'user_to_notification'
1115 1125 __table_args__ = (UniqueConstraint('user_id', 'notification_id'),
1116 1126 {'extend_existing': True})
1117 1127 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), primary_key=True)
1118 1128 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1119 1129 read = Column('read', Boolean, default=False)
1120 1130 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1121 1131
1122 1132 user = relationship('User', lazy="joined")
1123 1133 notification = relationship('Notification', lazy="joined",
1124 1134 order_by=lambda:Notification.created_on.desc(),)
1125 1135
1126 1136 def mark_as_read(self):
1127 1137 self.read = True
1128 1138 Session.add(self)
1129 1139
1130 1140
1131 1141 class DbMigrateVersion(Base, BaseModel):
1132 1142 __tablename__ = 'db_migrate_version'
1133 1143 __table_args__ = {'extend_existing': True}
1134 1144 repository_id = Column('repository_id', String(250), primary_key=True)
1135 1145 repository_path = Column('repository_path', Text)
1136 1146 version = Column('version', Integer)
@@ -1,723 +1,749 b''
1 1 """ this is forms validation classes
2 2 http://formencode.org/module-formencode.validators.html
3 3 for list off all availible validators
4 4
5 5 we can create our own validators
6 6
7 7 The table below outlines the options which can be used in a schema in addition to the validators themselves
8 8 pre_validators [] These validators will be applied before the schema
9 9 chained_validators [] These validators will be applied after the schema
10 10 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
11 11 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
12 12 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
13 13 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
14 14
15 15
16 16 <name> = formencode.validators.<name of validator>
17 17 <name> must equal form name
18 18 list=[1,2,3,4,5]
19 19 for SELECT use formencode.All(OneOf(list), Int())
20 20
21 21 """
22 22 import os
23 23 import re
24 24 import logging
25 25 import traceback
26 26
27 27 import formencode
28 28 from formencode import All
29 29 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
30 30 Email, Bool, StringBoolean, Set
31 31
32 32 from pylons.i18n.translation import _
33 33 from webhelpers.pylonslib.secure_form import authentication_token
34 34
35 35 from rhodecode.config.routing import ADMIN_PREFIX
36 36 from rhodecode.lib.utils import repo_name_slug
37 37 from rhodecode.lib.auth import authenticate, get_crypt_password
38 38 from rhodecode.lib.exceptions import LdapImportError
39 39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 40 from rhodecode import BACKENDS
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 #this is needed to translate the messages using _() in validators
46 46 class State_obj(object):
47 47 _ = staticmethod(_)
48 48
49 49
50 50 #==============================================================================
51 51 # VALIDATORS
52 52 #==============================================================================
53 53 class ValidAuthToken(formencode.validators.FancyValidator):
54 messages = {'invalid_token':_('Token mismatch')}
54 messages = {'invalid_token': _('Token mismatch')}
55 55
56 56 def validate_python(self, value, state):
57 57
58 58 if value != authentication_token():
59 raise formencode.Invalid(self.message('invalid_token', state,
60 search_number=value), value, state)
59 raise formencode.Invalid(
60 self.message('invalid_token',
61 state, search_number=value),
62 value,
63 state
64 )
61 65
62 66
63 67 def ValidUsername(edit, old_data):
64 68 class _ValidUsername(formencode.validators.FancyValidator):
65 69
66 70 def validate_python(self, value, state):
67 71 if value in ['default', 'new_user']:
68 72 raise formencode.Invalid(_('Invalid username'), value, state)
69 73 #check if user is unique
70 74 old_un = None
71 75 if edit:
72 76 old_un = User.get(old_data.get('user_id')).username
73 77
74 78 if old_un != value or not edit:
75 79 if User.get_by_username(value, case_insensitive=True):
76 80 raise formencode.Invalid(_('This username already '
77 81 'exists') , value, state)
78 82
79 83 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
80 raise formencode.Invalid(_('Username may only contain '
81 'alphanumeric characters '
82 'underscores, periods or dashes '
83 'and must begin with alphanumeric '
84 'character'), value, state)
84 raise formencode.Invalid(
85 _('Username may only contain alphanumeric characters '
86 'underscores, periods or dashes and must begin with '
87 'alphanumeric character'),
88 value,
89 state
90 )
85 91
86 92 return _ValidUsername
87 93
88 94
89 95 def ValidUsersGroup(edit, old_data):
90 96
91 97 class _ValidUsersGroup(formencode.validators.FancyValidator):
92 98
93 99 def validate_python(self, value, state):
94 100 if value in ['default']:
95 101 raise formencode.Invalid(_('Invalid group name'), value, state)
96 102 #check if group is unique
97 103 old_ugname = None
98 104 if edit:
99 105 old_ugname = UsersGroup.get(
100 106 old_data.get('users_group_id')).users_group_name
101 107
102 108 if old_ugname != value or not edit:
103 109 if UsersGroup.get_by_group_name(value, cache=False,
104 110 case_insensitive=True):
105 111 raise formencode.Invalid(_('This users group '
106 'already exists') , value,
112 'already exists'), value,
107 113 state)
108 114
109 115 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
110 raise formencode.Invalid(_('RepoGroup name may only contain '
111 'alphanumeric characters '
112 'underscores, periods or dashes '
113 'and must begin with alphanumeric '
114 'character'), value, state)
116 raise formencode.Invalid(
117 _('RepoGroup name may only contain alphanumeric characters '
118 'underscores, periods or dashes and must begin with '
119 'alphanumeric character'),
120 value,
121 state
122 )
115 123
116 124 return _ValidUsersGroup
117 125
118 126
119 127 def ValidReposGroup(edit, old_data):
120 128 class _ValidReposGroup(formencode.validators.FancyValidator):
121 129
122 130 def validate_python(self, value, state):
123 131 # TODO WRITE VALIDATIONS
124 132 group_name = value.get('group_name')
125 133 group_parent_id = value.get('group_parent_id')
126 134
127 135 # slugify repo group just in case :)
128 136 slug = repo_name_slug(group_name)
129 137
130 138 # check for parent of self
131 139 parent_of_self = lambda: (
132 140 old_data['group_id'] == int(group_parent_id)
133 141 if group_parent_id else False
134 142 )
135 143 if edit and parent_of_self():
136 144 e_dict = {
137 145 'group_parent_id': _('Cannot assign this group as parent')
138 146 }
139 147 raise formencode.Invalid('', value, state,
140 148 error_dict=e_dict)
141 149
142 150 old_gname = None
143 151 if edit:
144 152 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
145 153
146 154 if old_gname != group_name or not edit:
147 155
148 156 # check group
149 157 gr = RepoGroup.query()\
150 158 .filter(RepoGroup.group_name == slug)\
151 159 .filter(RepoGroup.group_parent_id == group_parent_id)\
152 160 .scalar()
153 161
154 162 if gr:
155 163 e_dict = {
156 164 'group_name': _('This group already exists')
157 165 }
158 166 raise formencode.Invalid('', value, state,
159 167 error_dict=e_dict)
160 168
161 169 # check for same repo
162 170 repo = Repository.query()\
163 171 .filter(Repository.repo_name == slug)\
164 172 .scalar()
165 173
166 174 if repo:
167 175 e_dict = {
168 176 'group_name': _('Repository with this name already exists')
169 177 }
170 178 raise formencode.Invalid('', value, state,
171 179 error_dict=e_dict)
172 180
173 181 return _ValidReposGroup
174 182
175 183
176 184 class ValidPassword(formencode.validators.FancyValidator):
177 185
178 186 def to_python(self, value, state):
179 187
180 if value:
188 if not value:
189 return
181 190
182 if value.get('password'):
183 try:
184 value['password'] = get_crypt_password(value['password'])
185 except UnicodeEncodeError:
186 e_dict = {'password':_('Invalid characters in password')}
187 raise formencode.Invalid('', value, state, error_dict=e_dict)
191 if value.get('password'):
192 try:
193 value['password'] = get_crypt_password(value['password'])
194 except UnicodeEncodeError:
195 e_dict = {'password': _('Invalid characters in password')}
196 raise formencode.Invalid('', value, state, error_dict=e_dict)
188 197
189 if value.get('password_confirmation'):
190 try:
191 value['password_confirmation'] = \
192 get_crypt_password(value['password_confirmation'])
193 except UnicodeEncodeError:
194 e_dict = {'password_confirmation':_('Invalid characters in password')}
195 raise formencode.Invalid('', value, state, error_dict=e_dict)
198 if value.get('password_confirmation'):
199 try:
200 value['password_confirmation'] = \
201 get_crypt_password(value['password_confirmation'])
202 except UnicodeEncodeError:
203 e_dict = {
204 'password_confirmation': _('Invalid characters in password')
205 }
206 raise formencode.Invalid('', value, state, error_dict=e_dict)
196 207
197 if value.get('new_password'):
198 try:
199 value['new_password'] = \
200 get_crypt_password(value['new_password'])
201 except UnicodeEncodeError:
202 e_dict = {'new_password':_('Invalid characters in password')}
203 raise formencode.Invalid('', value, state, error_dict=e_dict)
208 if value.get('new_password'):
209 try:
210 value['new_password'] = \
211 get_crypt_password(value['new_password'])
212 except UnicodeEncodeError:
213 e_dict = {'new_password': _('Invalid characters in password')}
214 raise formencode.Invalid('', value, state, error_dict=e_dict)
204 215
205 return value
216 return value
206 217
207 218
208 219 class ValidPasswordsMatch(formencode.validators.FancyValidator):
209 220
210 221 def validate_python(self, value, state):
211 222
212 223 pass_val = value.get('password') or value.get('new_password')
213 224 if pass_val != value['password_confirmation']:
214 225 e_dict = {'password_confirmation':
215 226 _('Passwords do not match')}
216 227 raise formencode.Invalid('', value, state, error_dict=e_dict)
217 228
218 229
219 230 class ValidAuth(formencode.validators.FancyValidator):
220 231 messages = {
221 232 'invalid_password':_('invalid password'),
222 233 'invalid_login':_('invalid user name'),
223 234 'disabled_account':_('Your account is disabled')
224 235 }
225 236
226 237 # error mapping
227 e_dict = {'username':messages['invalid_login'],
228 'password':messages['invalid_password']}
229 e_dict_disable = {'username':messages['disabled_account']}
238 e_dict = {'username': messages['invalid_login'],
239 'password': messages['invalid_password']}
240 e_dict_disable = {'username': messages['disabled_account']}
230 241
231 242 def validate_python(self, value, state):
232 243 password = value['password']
233 244 username = value['username']
234 245 user = User.get_by_username(username)
235 246
236 247 if authenticate(username, password):
237 248 return value
238 249 else:
239 250 if user and user.active is False:
240 251 log.warning('user %s is disabled', username)
241 raise formencode.Invalid(self.message('disabled_account',
242 state=State_obj),
243 value, state,
244 error_dict=self.e_dict_disable)
252 raise formencode.Invalid(
253 self.message('disabled_account',
254 state=State_obj),
255 value, state,
256 error_dict=self.e_dict_disable
257 )
245 258 else:
246 259 log.warning('user %s not authenticated', username)
247 raise formencode.Invalid(self.message('invalid_password',
248 state=State_obj), value, state,
249 error_dict=self.e_dict)
260 raise formencode.Invalid(
261 self.message('invalid_password',
262 state=State_obj), value, state,
263 error_dict=self.e_dict
264 )
250 265
251 266
252 267 class ValidRepoUser(formencode.validators.FancyValidator):
253 268
254 269 def to_python(self, value, state):
255 270 try:
256 271 User.query().filter(User.active == True)\
257 272 .filter(User.username == value).one()
258 273 except Exception:
259 274 raise formencode.Invalid(_('This username is not valid'),
260 275 value, state)
261 276 return value
262 277
263 278
264 279 def ValidRepoName(edit, old_data):
265 280 class _ValidRepoName(formencode.validators.FancyValidator):
266 281 def to_python(self, value, state):
267 282
268 283 repo_name = value.get('repo_name')
269 284
270 285 slug = repo_name_slug(repo_name)
271 286 if slug in [ADMIN_PREFIX, '']:
272 287 e_dict = {'repo_name': _('This repository name is disallowed')}
273 288 raise formencode.Invalid('', value, state, error_dict=e_dict)
274 289
275
276 290 if value.get('repo_group'):
277 291 gr = RepoGroup.get(value.get('repo_group'))
278 292 group_path = gr.full_path
279 293 # value needs to be aware of group name in order to check
280 294 # db key This is an actual just the name to store in the
281 295 # database
282 296 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
283 297
284 298 else:
285 299 group_path = ''
286 300 repo_name_full = repo_name
287 301
288
289 302 value['repo_name_full'] = repo_name_full
290 303 rename = old_data.get('repo_name') != repo_name_full
291 304 create = not edit
292 305 if rename or create:
293 306
294 307 if group_path != '':
295 308 if Repository.get_by_repo_name(repo_name_full):
296 e_dict = {'repo_name':_('This repository already '
297 'exists in a group "%s"') %
298 gr.group_name}
309 e_dict = {
310 'repo_name': _('This repository already exists in '
311 'a group "%s"') % gr.group_name
312 }
299 313 raise formencode.Invalid('', value, state,
300 314 error_dict=e_dict)
301 315 elif RepoGroup.get_by_group_name(repo_name_full):
302 e_dict = {'repo_name':_('There is a group with this'
303 ' name already "%s"') %
304 repo_name_full}
316 e_dict = {
317 'repo_name': _('There is a group with this name '
318 'already "%s"') % repo_name_full
319 }
305 320 raise formencode.Invalid('', value, state,
306 321 error_dict=e_dict)
307 322
308 323 elif Repository.get_by_repo_name(repo_name_full):
309 e_dict = {'repo_name':_('This repository '
324 e_dict = {'repo_name': _('This repository '
310 325 'already exists')}
311 326 raise formencode.Invalid('', value, state,
312 327 error_dict=e_dict)
313 328
314 329 return value
315 330
316 331 return _ValidRepoName
317 332
318 333
319 334 def ValidForkName(*args, **kwargs):
320 335 return ValidRepoName(*args, **kwargs)
321 336
322 337
323 338 def SlugifyName():
324 339 class _SlugifyName(formencode.validators.FancyValidator):
325 340
326 341 def to_python(self, value, state):
327 342 return repo_name_slug(value)
328 343
329 344 return _SlugifyName
330 345
331 346
332 347 def ValidCloneUri():
333 348 from mercurial.httprepo import httprepository, httpsrepository
334 349 from rhodecode.lib.utils import make_ui
335 350
336 351 class _ValidCloneUri(formencode.validators.FancyValidator):
337 352
338 353 def to_python(self, value, state):
339 354 if not value:
340 355 pass
341 356 elif value.startswith('https'):
342 357 try:
343 358 httpsrepository(make_ui('db'), value).capabilities
344 except Exception, e:
359 except Exception:
345 360 log.error(traceback.format_exc())
346 361 raise formencode.Invalid(_('invalid clone url'), value,
347 362 state)
348 363 elif value.startswith('http'):
349 364 try:
350 365 httprepository(make_ui('db'), value).capabilities
351 except Exception, e:
366 except Exception:
352 367 log.error(traceback.format_exc())
353 368 raise formencode.Invalid(_('invalid clone url'), value,
354 369 state)
355 370 else:
356 371 raise formencode.Invalid(_('Invalid clone url, provide a '
357 372 'valid clone http\s url'), value,
358 373 state)
359 374 return value
360 375
361 376 return _ValidCloneUri
362 377
363 378
364 379 def ValidForkType(old_data):
365 380 class _ValidForkType(formencode.validators.FancyValidator):
366 381
367 382 def to_python(self, value, state):
368 383 if old_data['repo_type'] != value:
369 384 raise formencode.Invalid(_('Fork have to be the same '
370 385 'type as original'), value, state)
371 386
372 387 return value
373 388 return _ValidForkType
374 389
375 390
376 391 class ValidPerms(formencode.validators.FancyValidator):
377 messages = {'perm_new_member_name':_('This username or users group name'
392 messages = {'perm_new_member_name': _('This username or users group name'
378 393 ' is not valid')}
379 394
380 395 def to_python(self, value, state):
381 396 perms_update = []
382 397 perms_new = []
383 398 #build a list of permission to update and new permission to create
384 399 for k, v in value.items():
385 400 #means new added member to permissions
386 401 if k.startswith('perm_new_member'):
387 402 new_perm = value.get('perm_new_member', False)
388 403 new_member = value.get('perm_new_member_name', False)
389 404 new_type = value.get('perm_new_member_type')
390 405
391 406 if new_member and new_perm:
392 407 if (new_member, new_perm, new_type) not in perms_new:
393 408 perms_new.append((new_member, new_perm, new_type))
394 409 elif k.startswith('u_perm_') or k.startswith('g_perm_'):
395 410 member = k[7:]
396 t = {'u':'user',
397 'g':'users_group'}[k[0]]
411 t = {'u': 'user',
412 'g': 'users_group'
413 }[k[0]]
398 414 if member == 'default':
399 415 if value['private']:
400 416 #set none for default when updating to private repo
401 417 v = 'repository.none'
402 418 perms_update.append((member, v, t))
403 419
404 420 value['perms_updates'] = perms_update
405 421 value['perms_new'] = perms_new
406 422
407 423 #update permissions
408 424 for k, v, t in perms_new:
409 425 try:
410 426 if t is 'user':
411 427 self.user_db = User.query()\
412 428 .filter(User.active == True)\
413 429 .filter(User.username == k).one()
414 430 if t is 'users_group':
415 431 self.user_db = UsersGroup.query()\
416 432 .filter(UsersGroup.users_group_active == True)\
417 433 .filter(UsersGroup.users_group_name == k).one()
418 434
419 435 except Exception:
420 436 msg = self.message('perm_new_member_name',
421 437 state=State_obj)
422 raise formencode.Invalid(msg, value, state,
423 error_dict={'perm_new_member_name':msg})
438 raise formencode.Invalid(
439 msg, value, state, error_dict={'perm_new_member_name': msg}
440 )
424 441 return value
425 442
426 443
427 444 class ValidSettings(formencode.validators.FancyValidator):
428 445
429 446 def to_python(self, value, state):
430 447 # settings form can't edit user
431 if value.has_key('user'):
448 if 'user' in value:
432 449 del['value']['user']
433
434 450 return value
435 451
436 452
437 453 class ValidPath(formencode.validators.FancyValidator):
438 454 def to_python(self, value, state):
439 455
440 456 if not os.path.isdir(value):
441 457 msg = _('This is not a valid path')
442 458 raise formencode.Invalid(msg, value, state,
443 error_dict={'paths_root_path':msg})
459 error_dict={'paths_root_path': msg})
444 460 return value
445 461
446 462
447 463 def UniqSystemEmail(old_data):
448 464 class _UniqSystemEmail(formencode.validators.FancyValidator):
449 465 def to_python(self, value, state):
450 466 value = value.lower()
451 if old_data.get('email','').lower() != value:
467 if old_data.get('email', '').lower() != value:
452 468 user = User.get_by_email(value, case_insensitive=True)
453 469 if user:
454 470 raise formencode.Invalid(
455 _("This e-mail address is already taken"),
456 value, state)
471 _("This e-mail address is already taken"), value, state
472 )
457 473 return value
458 474
459 475 return _UniqSystemEmail
460 476
461 477
462 478 class ValidSystemEmail(formencode.validators.FancyValidator):
463 479 def to_python(self, value, state):
464 480 value = value.lower()
465 481 user = User.get_by_email(value, case_insensitive=True)
466 482 if user is None:
467 raise formencode.Invalid(_("This e-mail address doesn't exist.") ,
468 value, state)
483 raise formencode.Invalid(
484 _("This e-mail address doesn't exist."), value, state
485 )
469 486
470 487 return value
471 488
472 489
473 490 class LdapLibValidator(formencode.validators.FancyValidator):
474 491
475 492 def to_python(self, value, state):
476 493
477 494 try:
478 495 import ldap
479 496 except ImportError:
480 497 raise LdapImportError
481 498 return value
482 499
483 500
484 501 class AttrLoginValidator(formencode.validators.FancyValidator):
485 502
486 503 def to_python(self, value, state):
487 504
488 505 if not value or not isinstance(value, (str, unicode)):
489 raise formencode.Invalid(_("The LDAP Login attribute of the CN "
490 "must be specified - this is the name "
491 "of the attribute that is equivalent "
492 "to 'username'"),
493 value, state)
506 raise formencode.Invalid(
507 _("The LDAP Login attribute of the CN must be specified - "
508 "this is the name of the attribute that is equivalent "
509 "to 'username'"), value, state
510 )
494 511
495 512 return value
496 513
514
497 515 #==============================================================================
498 516 # FORMS
499 517 #==============================================================================
500 518 class LoginForm(formencode.Schema):
501 519 allow_extra_fields = True
502 520 filter_extra_fields = True
503 521 username = UnicodeString(
504 strip=True,
505 min=1,
506 not_empty=True,
507 messages={
508 'empty':_('Please enter a login'),
509 'tooShort':_('Enter a value %(min)i characters long or more')}
510 )
522 strip=True,
523 min=1,
524 not_empty=True,
525 messages={
526 'empty': _('Please enter a login'),
527 'tooShort': _('Enter a value %(min)i characters long or more')}
528 )
511 529
512 530 password = UnicodeString(
513 strip=True,
514 min=3,
515 not_empty=True,
516 messages={
517 'empty':_('Please enter a password'),
518 'tooShort':_('Enter %(min)i characters or more')}
519 )
531 strip=True,
532 min=3,
533 not_empty=True,
534 messages={
535 'empty': _('Please enter a password'),
536 'tooShort': _('Enter %(min)i characters or more')}
537 )
520 538
521 539 remember = StringBoolean(if_missing=False)
522 540
523 541 chained_validators = [ValidAuth]
524 542
525 543
526 544 def UserForm(edit=False, old_data={}):
527 545 class _UserForm(formencode.Schema):
528 546 allow_extra_fields = True
529 547 filter_extra_fields = True
530 548 username = All(UnicodeString(strip=True, min=1, not_empty=True),
531 549 ValidUsername(edit, old_data))
532 550 if edit:
533 551 new_password = All(UnicodeString(strip=True, min=6, not_empty=False))
534 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=False))
552 password_confirmation = All(UnicodeString(strip=True, min=6,
553 not_empty=False))
535 554 admin = StringBoolean(if_missing=False)
536 555 else:
537 556 password = All(UnicodeString(strip=True, min=6, not_empty=True))
538 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=False))
557 password_confirmation = All(UnicodeString(strip=True, min=6,
558 not_empty=False))
539 559
540 560 active = StringBoolean(if_missing=False)
541 name = UnicodeString(strip=True, min=1, not_empty=True)
542 lastname = UnicodeString(strip=True, min=1, not_empty=True)
561 name = UnicodeString(strip=True, min=1, not_empty=False)
562 lastname = UnicodeString(strip=True, min=1, not_empty=False)
543 563 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
544 564
545 565 chained_validators = [ValidPasswordsMatch, ValidPassword]
546 566
547 567 return _UserForm
548 568
549 569
550 570 def UsersGroupForm(edit=False, old_data={}, available_members=[]):
551 571 class _UsersGroupForm(formencode.Schema):
552 572 allow_extra_fields = True
553 573 filter_extra_fields = True
554 574
555 575 users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
556 576 ValidUsersGroup(edit, old_data))
557 577
558 578 users_group_active = StringBoolean(if_missing=False)
559 579
560 580 if edit:
561 581 users_group_members = OneOf(available_members, hideList=False,
562 582 testValueList=True,
563 583 if_missing=None, not_empty=False)
564 584
565 585 return _UsersGroupForm
566 586
567 587
568 588 def ReposGroupForm(edit=False, old_data={}, available_groups=[]):
569 589 class _ReposGroupForm(formencode.Schema):
570 590 allow_extra_fields = True
571 591 filter_extra_fields = True
572 592
573 593 group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
574 594 SlugifyName())
575 595 group_description = UnicodeString(strip=True, min=1,
576 596 not_empty=True)
577 597 group_parent_id = OneOf(available_groups, hideList=False,
578 598 testValueList=True,
579 599 if_missing=None, not_empty=False)
580 600
581 601 chained_validators = [ValidReposGroup(edit, old_data)]
582 602
583 603 return _ReposGroupForm
584 604
585 605
586 606 def RegisterForm(edit=False, old_data={}):
587 607 class _RegisterForm(formencode.Schema):
588 608 allow_extra_fields = True
589 609 filter_extra_fields = True
590 610 username = All(ValidUsername(edit, old_data),
591 611 UnicodeString(strip=True, min=1, not_empty=True))
592 612 password = All(UnicodeString(strip=True, min=6, not_empty=True))
593 613 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
594 614 active = StringBoolean(if_missing=False)
595 name = UnicodeString(strip=True, min=1, not_empty=True)
596 lastname = UnicodeString(strip=True, min=1, not_empty=True)
615 name = UnicodeString(strip=True, min=1, not_empty=False)
616 lastname = UnicodeString(strip=True, min=1, not_empty=False)
597 617 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
598 618
599 619 chained_validators = [ValidPasswordsMatch, ValidPassword]
600 620
601 621 return _RegisterForm
602 622
623
603 624 def PasswordResetForm():
604 625 class _PasswordResetForm(formencode.Schema):
605 626 allow_extra_fields = True
606 627 filter_extra_fields = True
607 628 email = All(ValidSystemEmail(), Email(not_empty=True))
608 629 return _PasswordResetForm
609 630
631
610 632 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
611 633 repo_groups=[]):
612 634 class _RepoForm(formencode.Schema):
613 635 allow_extra_fields = True
614 636 filter_extra_fields = False
615 637 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
616 638 SlugifyName())
617 639 clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False),
618 640 ValidCloneUri()())
619 641 repo_group = OneOf(repo_groups, hideList=True)
620 642 repo_type = OneOf(supported_backends)
621 643 description = UnicodeString(strip=True, min=1, not_empty=True)
622 644 private = StringBoolean(if_missing=False)
623 645 enable_statistics = StringBoolean(if_missing=False)
624 646 enable_downloads = StringBoolean(if_missing=False)
625 647
626 648 if edit:
627 649 #this is repo owner
628 650 user = All(UnicodeString(not_empty=True), ValidRepoUser)
629 651
630 652 chained_validators = [ValidRepoName(edit, old_data), ValidPerms]
631 653 return _RepoForm
632 654
655
633 656 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
634 657 repo_groups=[]):
635 658 class _RepoForkForm(formencode.Schema):
636 659 allow_extra_fields = True
637 660 filter_extra_fields = False
638 661 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
639 662 SlugifyName())
640 663 repo_group = OneOf(repo_groups, hideList=True)
641 664 repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
642 665 description = UnicodeString(strip=True, min=1, not_empty=True)
643 666 private = StringBoolean(if_missing=False)
644 667 copy_permissions = StringBoolean(if_missing=False)
645 668 update_after_clone = StringBoolean(if_missing=False)
646 669 fork_parent_id = UnicodeString()
647 670 chained_validators = [ValidForkName(edit, old_data)]
648 671
649 672 return _RepoForkForm
650 673
674
651 675 def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
652 676 repo_groups=[]):
653 677 class _RepoForm(formencode.Schema):
654 678 allow_extra_fields = True
655 679 filter_extra_fields = False
656 680 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
657 681 SlugifyName())
658 682 description = UnicodeString(strip=True, min=1, not_empty=True)
659 683 repo_group = OneOf(repo_groups, hideList=True)
660 684 private = StringBoolean(if_missing=False)
661 685
662 686 chained_validators = [ValidRepoName(edit, old_data), ValidPerms,
663 687 ValidSettings]
664 688 return _RepoForm
665 689
666 690
667 691 def ApplicationSettingsForm():
668 692 class _ApplicationSettingsForm(formencode.Schema):
669 693 allow_extra_fields = True
670 694 filter_extra_fields = False
671 695 rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
672 696 rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
673 697 rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
674 698
675 699 return _ApplicationSettingsForm
676 700
701
677 702 def ApplicationUiSettingsForm():
678 703 class _ApplicationUiSettingsForm(formencode.Schema):
679 704 allow_extra_fields = True
680 705 filter_extra_fields = False
681 706 web_push_ssl = OneOf(['true', 'false'], if_missing='false')
682 707 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
683 708 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
684 709 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
685 710 hooks_pretxnchangegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
686 711 hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
687 712
688 713 return _ApplicationUiSettingsForm
689 714
715
690 716 def DefaultPermissionsForm(perms_choices, register_choices, create_choices):
691 717 class _DefaultPermissionsForm(formencode.Schema):
692 718 allow_extra_fields = True
693 719 filter_extra_fields = True
694 720 overwrite_default = StringBoolean(if_missing=False)
695 721 anonymous = OneOf(['True', 'False'], if_missing=False)
696 722 default_perm = OneOf(perms_choices)
697 723 default_register = OneOf(register_choices)
698 724 default_create = OneOf(create_choices)
699 725
700 726 return _DefaultPermissionsForm
701 727
702 728
703 729 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices, tls_kind_choices):
704 730 class _LdapSettingsForm(formencode.Schema):
705 731 allow_extra_fields = True
706 732 filter_extra_fields = True
707 733 pre_validators = [LdapLibValidator]
708 734 ldap_active = StringBoolean(if_missing=False)
709 735 ldap_host = UnicodeString(strip=True,)
710 736 ldap_port = Number(strip=True,)
711 737 ldap_tls_kind = OneOf(tls_kind_choices)
712 738 ldap_tls_reqcert = OneOf(tls_reqcert_choices)
713 739 ldap_dn_user = UnicodeString(strip=True,)
714 740 ldap_dn_pass = UnicodeString(strip=True,)
715 741 ldap_base_dn = UnicodeString(strip=True,)
716 742 ldap_filter = UnicodeString(strip=True,)
717 743 ldap_search_scope = OneOf(search_scope_choices)
718 744 ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
719 745 ldap_attr_firstname = UnicodeString(strip=True,)
720 746 ldap_attr_lastname = UnicodeString(strip=True,)
721 747 ldap_attr_email = UnicodeString(strip=True,)
722 748
723 749 return _LdapSettingsForm
@@ -1,507 +1,504 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import url
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.lib import safe_unicode
33 33 from rhodecode.lib.caching_query import FromCache
34 34
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 38 Notification
39 39 from rhodecode.lib.exceptions import DefaultUserException, \
40 40 UserOwnsReposException
41 41
42 42 from sqlalchemy.exc import DatabaseError
43 43 from rhodecode.lib import generate_api_key
44 44 from sqlalchemy.orm import joinedload
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 PERM_WEIGHTS = {'repository.none': 0,
50 50 'repository.read': 1,
51 51 'repository.write': 3,
52 52 'repository.admin': 3}
53 53
54 54
55 55 class UserModel(BaseModel):
56 56
57 57 def __get_user(self, user):
58 58 return self._get_instance(User, user)
59 59
60 60 def get(self, user_id, cache=False):
61 61 user = self.sa.query(User)
62 62 if cache:
63 63 user = user.options(FromCache("sql_cache_short",
64 64 "get_user_%s" % user_id))
65 65 return user.get(user_id)
66 66
67 67 def get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_api_key(self, api_key, cache=False):
80 80 return User.get_by_api_key(api_key, cache)
81 81
82 82 def create(self, form_data):
83 83 try:
84 84 new_user = User()
85 85 for k, v in form_data.items():
86 86 setattr(new_user, k, v)
87 87
88 88 new_user.api_key = generate_api_key(form_data['username'])
89 89 self.sa.add(new_user)
90 90 return new_user
91 91 except:
92 92 log.error(traceback.format_exc())
93 93 raise
94 94
95
96 95 def create_or_update(self, username, password, email, name, lastname,
97 96 active=True, admin=False, ldap_dn=None):
98 97 """
99 98 Creates a new instance if not found, or updates current one
100 99
101 100 :param username:
102 101 :param password:
103 102 :param email:
104 103 :param active:
105 104 :param name:
106 105 :param lastname:
107 106 :param active:
108 107 :param admin:
109 108 :param ldap_dn:
110 109 """
111 110
112 111 from rhodecode.lib.auth import get_crypt_password
113 112
114 113 log.debug('Checking for %s account in RhodeCode database', username)
115 114 user = User.get_by_username(username, case_insensitive=True)
116 115 if user is None:
117 116 log.debug('creating new user %s', username)
118 117 new_user = User()
119 118 else:
120 119 log.debug('updating user %s', username)
121 120 new_user = user
122 121
123 122 try:
124 123 new_user.username = username
125 124 new_user.admin = admin
126 125 new_user.password = get_crypt_password(password)
127 126 new_user.api_key = generate_api_key(username)
128 127 new_user.email = email
129 128 new_user.active = active
130 129 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
131 130 new_user.name = name
132 131 new_user.lastname = lastname
133 132 self.sa.add(new_user)
134 133 return new_user
135 134 except (DatabaseError,):
136 135 log.error(traceback.format_exc())
137 136 raise
138 137
139
140 138 def create_for_container_auth(self, username, attrs):
141 139 """
142 140 Creates the given user if it's not already in the database
143 141
144 142 :param username:
145 143 :param attrs:
146 144 """
147 145 if self.get_by_username(username, case_insensitive=True) is None:
148 146
149 147 # autogenerate email for container account without one
150 148 generate_email = lambda usr: '%s@container_auth.account' % usr
151 149
152 150 try:
153 151 new_user = User()
154 152 new_user.username = username
155 153 new_user.password = None
156 154 new_user.api_key = generate_api_key(username)
157 155 new_user.email = attrs['email']
158 156 new_user.active = attrs.get('active', True)
159 157 new_user.name = attrs['name'] or generate_email(username)
160 158 new_user.lastname = attrs['lastname']
161 159
162 160 self.sa.add(new_user)
163 161 return new_user
164 162 except (DatabaseError,):
165 163 log.error(traceback.format_exc())
166 164 self.sa.rollback()
167 165 raise
168 166 log.debug('User %s already exists. Skipping creation of account'
169 167 ' for container auth.', username)
170 168 return None
171 169
172 170 def create_ldap(self, username, password, user_dn, attrs):
173 171 """
174 172 Checks if user is in database, if not creates this user marked
175 173 as ldap user
176 174
177 175 :param username:
178 176 :param password:
179 177 :param user_dn:
180 178 :param attrs:
181 179 """
182 180 from rhodecode.lib.auth import get_crypt_password
183 181 log.debug('Checking for such ldap account in RhodeCode database')
184 182 if self.get_by_username(username, case_insensitive=True) is None:
185 183
186 184 # autogenerate email for ldap account without one
187 185 generate_email = lambda usr: '%s@ldap.account' % usr
188 186
189 187 try:
190 188 new_user = User()
191 189 username = username.lower()
192 190 # add ldap account always lowercase
193 191 new_user.username = username
194 192 new_user.password = get_crypt_password(password)
195 193 new_user.api_key = generate_api_key(username)
196 194 new_user.email = attrs['email'] or generate_email(username)
197 195 new_user.active = attrs.get('active', True)
198 196 new_user.ldap_dn = safe_unicode(user_dn)
199 197 new_user.name = attrs['name']
200 198 new_user.lastname = attrs['lastname']
201 199
202 200 self.sa.add(new_user)
203 201 return new_user
204 202 except (DatabaseError,):
205 203 log.error(traceback.format_exc())
206 204 self.sa.rollback()
207 205 raise
208 206 log.debug('this %s user exists skipping creation of ldap account',
209 207 username)
210 208 return None
211 209
212 210 def create_registration(self, form_data):
213 211 from rhodecode.model.notification import NotificationModel
214 212
215 213 try:
216 214 new_user = User()
217 215 for k, v in form_data.items():
218 216 if k != 'admin':
219 217 setattr(new_user, k, v)
220 218
221 219 self.sa.add(new_user)
222 220 self.sa.flush()
223 221
224 222 # notification to admins
225 223 subject = _('new user registration')
226 224 body = ('New user registration\n'
227 225 '---------------------\n'
228 226 '- Username: %s\n'
229 227 '- Full Name: %s\n'
230 228 '- Email: %s\n')
231 229 body = body % (new_user.username, new_user.full_name,
232 230 new_user.email)
233 231 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
234 kw = {'registered_user_url':edit_url}
232 kw = {'registered_user_url': edit_url}
235 233 NotificationModel().create(created_by=new_user, subject=subject,
236 234 body=body, recipients=None,
237 235 type_=Notification.TYPE_REGISTRATION,
238 236 email_kwargs=kw)
239 237
240 238 except:
241 239 log.error(traceback.format_exc())
242 240 raise
243 241
244 242 def update(self, user_id, form_data):
245 243 try:
246 244 user = self.get(user_id, cache=False)
247 245 if user.username == 'default':
248 246 raise DefaultUserException(
249 247 _("You can't Edit this user since it's"
250 248 " crucial for entire application"))
251 249
252 250 for k, v in form_data.items():
253 251 if k == 'new_password' and v != '':
254 252 user.password = v
255 253 user.api_key = generate_api_key(user.username)
256 254 else:
257 255 setattr(user, k, v)
258 256
259 257 self.sa.add(user)
260 258 except:
261 259 log.error(traceback.format_exc())
262 260 raise
263 261
264 262 def update_my_account(self, user_id, form_data):
265 263 try:
266 264 user = self.get(user_id, cache=False)
267 265 if user.username == 'default':
268 266 raise DefaultUserException(
269 267 _("You can't Edit this user since it's"
270 268 " crucial for entire application"))
271 269 for k, v in form_data.items():
272 270 if k == 'new_password' and v != '':
273 271 user.password = v
274 272 user.api_key = generate_api_key(user.username)
275 273 else:
276 274 if k not in ['admin', 'active']:
277 275 setattr(user, k, v)
278 276
279 277 self.sa.add(user)
280 278 except:
281 279 log.error(traceback.format_exc())
282 280 raise
283 281
284 282 def delete(self, user):
285 283 user = self.__get_user(user)
286 284
287 285 try:
288 286 if user.username == 'default':
289 287 raise DefaultUserException(
290 288 _("You can't remove this user since it's"
291 289 " crucial for entire application"))
292 290 if user.repositories:
293 291 raise UserOwnsReposException(_('This user still owns %s '
294 292 'repositories and cannot be '
295 293 'removed. Switch owners or '
296 294 'remove those repositories') \
297 295 % user.repositories)
298 296 self.sa.delete(user)
299 297 except:
300 298 log.error(traceback.format_exc())
301 299 raise
302 300
303 301 def reset_password_link(self, data):
304 302 from rhodecode.lib.celerylib import tasks, run_task
305 303 run_task(tasks.send_password_link, data['email'])
306 304
307 305 def reset_password(self, data):
308 306 from rhodecode.lib.celerylib import tasks, run_task
309 307 run_task(tasks.reset_user_password, data['email'])
310 308
311 309 def fill_data(self, auth_user, user_id=None, api_key=None):
312 310 """
313 311 Fetches auth_user by user_id,or api_key if present.
314 312 Fills auth_user attributes with those taken from database.
315 313 Additionally set's is_authenitated if lookup fails
316 314 present in database
317 315
318 316 :param auth_user: instance of user to set attributes
319 317 :param user_id: user id to fetch by
320 318 :param api_key: api key to fetch by
321 319 """
322 320 if user_id is None and api_key is None:
323 321 raise Exception('You need to pass user_id or api_key')
324 322
325 323 try:
326 324 if api_key:
327 325 dbuser = self.get_by_api_key(api_key)
328 326 else:
329 327 dbuser = self.get(user_id)
330 328
331 329 if dbuser is not None and dbuser.active:
332 330 log.debug('filling %s data', dbuser)
333 331 for k, v in dbuser.get_dict().items():
334 332 setattr(auth_user, k, v)
335 333 else:
336 334 return False
337 335
338 336 except:
339 337 log.error(traceback.format_exc())
340 338 auth_user.is_authenticated = False
341 339 return False
342 340
343 341 return True
344 342
345 343 def fill_perms(self, user):
346 344 """
347 345 Fills user permission attribute with permissions taken from database
348 346 works for permissions given for repositories, and for permissions that
349 347 are granted to groups
350 348
351 349 :param user: user instance to fill his perms
352 350 """
353 351
354 352 user.permissions['repositories'] = {}
355 353 user.permissions['global'] = set()
356 354
357 355 #======================================================================
358 356 # fetch default permissions
359 357 #======================================================================
360 358 default_user = User.get_by_username('default', cache=True)
361 359 default_user_id = default_user.user_id
362 360
363 361 default_perms = Permission.get_default_perms(default_user_id)
364 362
365 363 if user.is_admin:
366 364 #==================================================================
367 365 # #admin have all default rights set to admin
368 366 #==================================================================
369 367 user.permissions['global'].add('hg.admin')
370 368
371 369 for perm in default_perms:
372 370 p = 'repository.admin'
373 371 user.permissions['repositories'][perm.UserRepoToPerm.
374 372 repository.repo_name] = p
375 373
376 374 else:
377 375 #==================================================================
378 376 # set default permissions
379 377 #==================================================================
380 378 uid = user.user_id
381 379
382 380 # default global
383 381 default_global_perms = self.sa.query(UserToPerm)\
384 382 .filter(UserToPerm.user_id == default_user_id)
385 383
386 384 for perm in default_global_perms:
387 385 user.permissions['global'].add(perm.permission.permission_name)
388 386
389 387 # default for repositories
390 388 for perm in default_perms:
391 389 if perm.Repository.private and not (perm.Repository.user_id ==
392 390 uid):
393 391 # disable defaults for private repos,
394 392 p = 'repository.none'
395 393 elif perm.Repository.user_id == uid:
396 394 # set admin if owner
397 395 p = 'repository.admin'
398 396 else:
399 397 p = perm.Permission.permission_name
400 398
401 399 user.permissions['repositories'][perm.UserRepoToPerm.
402 400 repository.repo_name] = p
403 401
404 402 #==================================================================
405 403 # overwrite default with user permissions if any
406 404 #==================================================================
407 405
408 406 # user global
409 407 user_perms = self.sa.query(UserToPerm)\
410 408 .options(joinedload(UserToPerm.permission))\
411 409 .filter(UserToPerm.user_id == uid).all()
412 410
413 411 for perm in user_perms:
414 412 user.permissions['global'].add(perm.permission.permission_name)
415 413
416 414 # user repositories
417 415 user_repo_perms = self.sa.query(UserRepoToPerm, Permission,
418 416 Repository)\
419 417 .join((Repository, UserRepoToPerm.repository_id ==
420 418 Repository.repo_id))\
421 419 .join((Permission, UserRepoToPerm.permission_id ==
422 420 Permission.permission_id))\
423 421 .filter(UserRepoToPerm.user_id == uid).all()
424 422
425 423 for perm in user_repo_perms:
426 424 # set admin if owner
427 425 if perm.Repository.user_id == uid:
428 426 p = 'repository.admin'
429 427 else:
430 428 p = perm.Permission.permission_name
431 429 user.permissions['repositories'][perm.UserRepoToPerm.
432 430 repository.repo_name] = p
433 431
434 432 #==================================================================
435 433 # check if user is part of groups for this repository and fill in
436 434 # (or replace with higher) permissions
437 435 #==================================================================
438 436
439 437 # users group global
440 438 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
441 439 .options(joinedload(UsersGroupToPerm.permission))\
442 440 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
443 441 UsersGroupMember.users_group_id))\
444 442 .filter(UsersGroupMember.user_id == uid).all()
445 443
446 444 for perm in user_perms_from_users_groups:
447 445 user.permissions['global'].add(perm.permission.permission_name)
448 446
449 447 # users group repositories
450 448 user_repo_perms_from_users_groups = self.sa.query(
451 449 UsersGroupRepoToPerm,
452 450 Permission, Repository,)\
453 451 .join((Repository, UsersGroupRepoToPerm.repository_id ==
454 452 Repository.repo_id))\
455 453 .join((Permission, UsersGroupRepoToPerm.permission_id ==
456 454 Permission.permission_id))\
457 455 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id ==
458 456 UsersGroupMember.users_group_id))\
459 457 .filter(UsersGroupMember.user_id == uid).all()
460 458
461 459 for perm in user_repo_perms_from_users_groups:
462 460 p = perm.Permission.permission_name
463 461 cur_perm = user.permissions['repositories'][perm.
464 462 UsersGroupRepoToPerm.
465 463 repository.repo_name]
466 464 # overwrite permission only if it's greater than permission
467 465 # given from other sources
468 466 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
469 467 user.permissions['repositories'][perm.UsersGroupRepoToPerm.
470 468 repository.repo_name] = p
471 469
472 470 return user
473 471
474 472 def has_perm(self, user, perm):
475 473 if not isinstance(perm, Permission):
476 474 raise Exception('perm needs to be an instance of Permission class '
477 475 'got %s instead' % type(perm))
478 476
479 477 user = self.__get_user(user)
480 478
481 479 return UserToPerm.query().filter(UserToPerm.user == user)\
482 480 .filter(UserToPerm.permission == perm).scalar() is not None
483 481
484 482 def grant_perm(self, user, perm):
485 483 if not isinstance(perm, Permission):
486 484 raise Exception('perm needs to be an instance of Permission class '
487 485 'got %s instead' % type(perm))
488 486
489 487 user = self.__get_user(user)
490 488
491 489 new = UserToPerm()
492 490 new.user = user
493 491 new.permission = perm
494 492 self.sa.add(new)
495 493
496
497 494 def revoke_perm(self, user, perm):
498 495 if not isinstance(perm, Permission):
499 496 raise Exception('perm needs to be an instance of Permission class '
500 497 'got %s instead' % type(perm))
501 498
502 499 user = self.__get_user(user)
503 500
504 501 obj = UserToPerm.query().filter(UserToPerm.user == user)\
505 502 .filter(UserToPerm.permission == perm).scalar()
506 503 if obj:
507 504 self.sa.delete(obj)
@@ -1,100 +1,100 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Add user')} - ${c.rhodecode_name}
6 6 </%def>
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 9 &raquo;
10 10 ${h.link_to(_('Users'),h.url('users'))}
11 11 &raquo;
12 12 ${_('add new user')}
13 13 </%def>
14 14
15 15 <%def name="page_nav()">
16 16 ${self.menu('admin')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <!-- box / title -->
22 22 <div class="title">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25 <!-- end box / title -->
26 26 ${h.form(url('users'))}
27 27 <div class="form">
28 28 <!-- fields -->
29 29 <div class="fields">
30 30 <div class="field">
31 31 <div class="label">
32 32 <label for="username">${_('Username')}:</label>
33 33 </div>
34 34 <div class="input">
35 35 ${h.text('username',class_='small')}
36 36 </div>
37 37 </div>
38 38
39 39 <div class="field">
40 40 <div class="label">
41 41 <label for="password">${_('Password')}:</label>
42 42 </div>
43 43 <div class="input">
44 44 ${h.password('password',class_='small')}
45 45 </div>
46 46 </div>
47 47
48 48 <div class="field">
49 49 <div class="label">
50 50 <label for="password_confirmation">${_('Password confirmation')}:</label>
51 51 </div>
52 52 <div class="input">
53 53 ${h.password('password_confirmation',class_="small",autocomplete="off")}
54 54 </div>
55 55 </div>
56 56
57 57 <div class="field">
58 58 <div class="label">
59 59 <label for="name">${_('First Name')}:</label>
60 60 </div>
61 61 <div class="input">
62 62 ${h.text('name',class_='small')}
63 63 </div>
64 64 </div>
65 65
66 66 <div class="field">
67 67 <div class="label">
68 68 <label for="lastname">${_('Last Name')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 ${h.text('lastname',class_='small')}
72 72 </div>
73 73 </div>
74 74
75 75 <div class="field">
76 76 <div class="label">
77 77 <label for="email">${_('Email')}:</label>
78 78 </div>
79 79 <div class="input">
80 80 ${h.text('email',class_='small')}
81 81 </div>
82 82 </div>
83 83
84 84 <div class="field">
85 85 <div class="label label-checkbox">
86 86 <label for="active">${_('Active')}:</label>
87 87 </div>
88 88 <div class="checkboxes">
89 ${h.checkbox('active',value=True)}
89 ${h.checkbox('active',value=True,checked='checked')}
90 90 </div>
91 91 </div>
92 92
93 93 <div class="buttons">
94 94 ${h.submit('save',_('save'),class_="ui-button")}
95 95 </div>
96 96 </div>
97 97 </div>
98 98 ${h.end_form()}
99 99 </div>
100 100 </%def>
@@ -1,336 +1,336 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <!-- HEADER -->
5 5 <div id="header">
6 6 <div id="header-inner" class="title">
7 7 <div id="logo">
8 8 <h1><a href="${h.url('home')}">${c.rhodecode_name}</a></h1>
9 9 </div>
10 10 <!-- MENU -->
11 11 ${self.page_nav()}
12 12 <!-- END MENU -->
13 13 ${self.body()}
14 14 </div>
15 15 </div>
16 16 <!-- END HEADER -->
17 17
18 18 <!-- CONTENT -->
19 19 <div id="content">
20 20 <div class="flash_msg">
21 21 <% messages = h.flash.pop_messages() %>
22 22 % if messages:
23 23 <ul id="flash-messages">
24 24 % for message in messages:
25 25 <li class="${message.category}_msg">${message}</li>
26 26 % endfor
27 27 </ul>
28 28 % endif
29 29 </div>
30 30 <div id="main">
31 31 ${next.main()}
32 32 </div>
33 33 </div>
34 34 <!-- END CONTENT -->
35 35
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title">
39 39 <div>
40 40 <p class="footer-link">
41 41 <a href="${h.url('bugtracker')}">${_('Submit a bug')}</a>
42 42 </p>
43 43 <p class="footer-link-right">
44 44 <a href="${h.url('rhodecode_official')}">RhodeCode</a>
45 45 ${c.rhodecode_version} &copy; 2010-${h.datetime.today().year} by Marcin Kuzminski
46 46 </p>
47 47 </div>
48 48 </div>
49 49 </div>
50 50 <!-- END FOOTER -->
51 51
52 52 ### MAKO DEFS ###
53 53 <%def name="page_nav()">
54 54 ${self.menu()}
55 55 </%def>
56 56
57 57 <%def name="breadcrumbs()">
58 58 <div class="breadcrumbs">
59 59 ${self.breadcrumbs_links()}
60 60 </div>
61 61 </%def>
62 62
63 63 <%def name="usermenu()">
64 64 <div class="user-menu">
65 65 <div class="container">
66 66 <div class="gravatar" id="quick_login_link">
67 67 <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,24)}" />
68 68 </div>
69 69 %if c.rhodecode_user.username != 'default' and c.unread_notifications != 0:
70 70 <div class="notifications">
71 71 <a id="notification_counter" href="${h.url('notifications')}">${c.unread_notifications}</a>
72 72 </div>
73 73 %endif
74 74 </div>
75 75 <div id="quick_login" style="display:none">
76 76 %if c.rhodecode_user.username == 'default':
77 77 <h4>${_('Login to your account')}</h4>
78 78 ${h.form(h.url('login_home',came_from=h.url.current()))}
79 79 <div class="form">
80 80 <div class="fields">
81 81 <div class="field">
82 82 <div class="label">
83 83 <label for="username">${_('Username')}:</label>
84 84 </div>
85 85 <div class="input">
86 86 ${h.text('username',class_='focus',size=40)}
87 87 </div>
88 88
89 89 </div>
90 90 <div class="field">
91 91 <div class="label">
92 92 <label for="password">${_('Password')}:</label>
93 93 </div>
94 94 <div class="input">
95 95 ${h.password('password',class_='focus',size=40)}
96 96 </div>
97 97
98 98 </div>
99 99 <div class="buttons">
100 100 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
101 101 <div class="register">
102 102 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
103 103 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
104 104 %endif
105 105 </div>
106 106 <div class="submit">
107 107 ${h.submit('sign_in',_('Log In'),class_="ui-btn xsmall")}
108 108 </div>
109 109 </div>
110 110 </div>
111 111 </div>
112 112 ${h.end_form()}
113 113 %else:
114 114 <div class="links_left">
115 <div class="full_name">${c.rhodecode_user.full_name}</div>
115 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
116 116 <div class="email">${c.rhodecode_user.email}</div>
117 117 <div class="big_gravatar"><img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,48)}" /></div>
118 118 </div>
119 119 <div class="links_right">
120 120 <ol class="links">
121 121 <li>${h.link_to(_(u'Home'),h.url('home'))}</li>
122 122 <li>${h.link_to(_(u'My account'),h.url('admin_settings_my_account'))}</li>
123 123 <li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
124 124 </ol>
125 125 </div>
126 126 %endif
127 127 </div>
128 128 </div>
129 129 </%def>
130 130
131 131 <%def name="menu(current=None)">
132 132 <%
133 133 def is_current(selected):
134 134 if selected == current:
135 135 return h.literal('class="current"')
136 136 %>
137 137 %if current not in ['home','admin']:
138 138 ##REGULAR MENU
139 139 <ul id="quick">
140 140 <!-- repo switcher -->
141 141 <li>
142 142 <a class="menu_link" id="repo_switcher" title="${_('Switch repository')}" href="#">
143 143 <span class="icon">
144 144 <img src="${h.url('/images/icons/database.png')}" alt="${_('Products')}" />
145 145 </span>
146 146 <span>&darr;</span>
147 147 </a>
148 148 <ul id="repo_switcher_list" class="repo_switcher">
149 149 <li>
150 150 <a href="#">${_('loading...')}</a>
151 151 </li>
152 152 </ul>
153 153 </li>
154 154
155 155 <li ${is_current('summary')}>
156 156 <a class="menu_link" title="${_('Summary')}" href="${h.url('summary_home',repo_name=c.repo_name)}">
157 157 <span class="icon">
158 158 <img src="${h.url('/images/icons/clipboard_16.png')}" alt="${_('Summary')}" />
159 159 </span>
160 160 <span>${_('Summary')}</span>
161 161 </a>
162 162 </li>
163 163 <li ${is_current('changelog')}>
164 164 <a class="menu_link" title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=c.repo_name)}">
165 165 <span class="icon">
166 166 <img src="${h.url('/images/icons/time.png')}" alt="${_('Changelog')}" />
167 167 </span>
168 168 <span>${_('Changelog')}</span>
169 169 </a>
170 170 </li>
171 171
172 172 <li ${is_current('switch_to')}>
173 173 <a class="menu_link" id="branch_tag_switcher" title="${_('Switch to')}" href="#">
174 174 <span class="icon">
175 175 <img src="${h.url('/images/icons/arrow_switch.png')}" alt="${_('Switch to')}" />
176 176 </span>
177 177 <span>${_('Switch to')}</span>
178 178 </a>
179 179 <ul id="switch_to_list" class="switch_to">
180 180 <li><a href="#">${_('loading...')}</a></li>
181 181 </ul>
182 182 </li>
183 183 <li ${is_current('files')}>
184 184 <a class="menu_link" title="${_('Files')}" href="${h.url('files_home',repo_name=c.repo_name)}">
185 185 <span class="icon">
186 186 <img src="${h.url('/images/icons/file.png')}" alt="${_('Files')}" />
187 187 </span>
188 188 <span>${_('Files')}</span>
189 189 </a>
190 190 </li>
191 191
192 192 <li ${is_current('options')}>
193 193 <a class="menu_link" title="${_('Options')}" href="#">
194 194 <span class="icon">
195 195 <img src="${h.url('/images/icons/table_gear.png')}" alt="${_('Admin')}" />
196 196 </span>
197 197 <span>${_('Options')}</span>
198 198 </a>
199 199 <ul>
200 200 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
201 201 %if h.HasPermissionAll('hg.admin')('access settings on repository'):
202 202 <li>${h.link_to(_('settings'),h.url('edit_repo',repo_name=c.repo_name),class_='settings')}</li>
203 203 %else:
204 204 <li>${h.link_to(_('settings'),h.url('repo_settings_home',repo_name=c.repo_name),class_='settings')}</li>
205 205 %endif
206 206 %endif
207 207 <li>${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}</li>
208 208 <li>${h.link_to(_('search'),h.url('search_repo',search_repo=c.repo_name),class_='search')}</li>
209 209
210 210 % if h.HasPermissionAll('hg.admin')('access admin main page'):
211 211 <li>
212 212 ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')}
213 213 <%def name="admin_menu()">
214 214 <ul>
215 215 <li>${h.link_to(_('journal'),h.url('admin_home'),class_='journal')}</li>
216 216 <li>${h.link_to(_('repositories'),h.url('repos'),class_='repos')}</li>
217 217 <li>${h.link_to(_('repositories groups'),h.url('repos_groups'),class_='repos_groups')}</li>
218 218 <li>${h.link_to(_('users'),h.url('users'),class_='users')}</li>
219 219 <li>${h.link_to(_('users groups'),h.url('users_groups'),class_='groups')}</li>
220 220 <li>${h.link_to(_('permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li>
221 221 <li>${h.link_to(_('ldap'),h.url('ldap_home'),class_='ldap')}</li>
222 222 <li class="last">${h.link_to(_('settings'),h.url('admin_settings'),class_='settings')}</li>
223 223 </ul>
224 224 </%def>
225 225
226 226 ${admin_menu()}
227 227 </li>
228 228 % endif
229 229 </ul>
230 230 </li>
231 231
232 232 <li>
233 233 <a class="menu_link" title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
234 234 <span class="icon_short">
235 235 <img src="${h.url('/images/icons/heart.png')}" alt="${_('Followers')}" />
236 236 </span>
237 237 <span id="current_followers_count" class="short">${c.repository_followers}</span>
238 238 </a>
239 239 </li>
240 240 <li>
241 241 <a class="menu_link" title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
242 242 <span class="icon_short">
243 243 <img src="${h.url('/images/icons/arrow_divide.png')}" alt="${_('Forks')}" />
244 244 </span>
245 245 <span class="short">${c.repository_forks}</span>
246 246 </a>
247 247 </li>
248 248 ${usermenu()}
249 249 </ul>
250 250 <script type="text/javascript">
251 251 YUE.on('repo_switcher','mouseover',function(){
252 252 function qfilter(){
253 253 var nodes = YUQ('ul#repo_switcher_list li a.repo_name');
254 254 var target = 'q_filter_rs';
255 255 var func = function(node){
256 256 return node.parentNode;
257 257 }
258 258 q_filter(target,nodes,func);
259 259 }
260 260 var loaded = YUD.hasClass('repo_switcher','loaded');
261 261 if(!loaded){
262 262 YUD.addClass('repo_switcher','loaded');
263 263 ypjax("${h.url('repo_switcher')}",'repo_switcher_list',
264 264 function(o){qfilter();},
265 265 function(o){YUD.removeClass('repo_switcher','loaded');}
266 266 ,null);
267 267 }
268 268 return false;
269 269 });
270 270
271 271 YUE.on('branch_tag_switcher','mouseover',function(){
272 272 var loaded = YUD.hasClass('branch_tag_switcher','loaded');
273 273 if(!loaded){
274 274 YUD.addClass('branch_tag_switcher','loaded');
275 275 ypjax("${h.url('branch_tag_switcher',repo_name=c.repo_name)}",'switch_to_list',
276 276 function(o){},
277 277 function(o){YUD.removeClass('branch_tag_switcher','loaded');}
278 278 ,null);
279 279 }
280 280 return false;
281 281 });
282 282 </script>
283 283 %else:
284 284 ##ROOT MENU
285 285 <ul id="quick">
286 286 <li>
287 287 <a class="menu_link" title="${_('Home')}" href="${h.url('home')}">
288 288 <span class="icon">
289 289 <img src="${h.url('/images/icons/home_16.png')}" alt="${_('Home')}" />
290 290 </span>
291 291 <span>${_('Home')}</span>
292 292 </a>
293 293 </li>
294 294 %if c.rhodecode_user.username != 'default':
295 295 <li>
296 296 <a class="menu_link" title="${_('Journal')}" href="${h.url('journal')}">
297 297 <span class="icon">
298 298 <img src="${h.url('/images/icons/book.png')}" alt="${_('Journal')}" />
299 299 </span>
300 300 <span>${_('Journal')}</span>
301 301 </a>
302 302 </li>
303 303 %else:
304 304 <li>
305 305 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
306 306 <span class="icon">
307 307 <img src="${h.url('/images/icons/book.png')}" alt="${_('Public journal')}" />
308 308 </span>
309 309 <span>${_('Public journal')}</span>
310 310 </a>
311 311 </li>
312 312 %endif
313 313 <li>
314 314 <a class="menu_link" title="${_('Search')}" href="${h.url('search')}">
315 315 <span class="icon">
316 316 <img src="${h.url('/images/icons/search_16.png')}" alt="${_('Search')}" />
317 317 </span>
318 318 <span>${_('Search')}</span>
319 319 </a>
320 320 </li>
321 321
322 322 %if h.HasPermissionAll('hg.admin')('access admin main page'):
323 323 <li ${is_current('admin')}>
324 324 <a class="menu_link" title="${_('Admin')}" href="${h.url('admin_home')}">
325 325 <span class="icon">
326 326 <img src="${h.url('/images/icons/cog_edit.png')}" alt="${_('Admin')}" />
327 327 </span>
328 328 <span>${_('Admin')}</span>
329 329 </a>
330 330 ${admin_menu()}
331 331 </li>
332 332 %endif
333 333 ${usermenu()}
334 334 </ul>
335 335 %endif
336 336 </%def>
General Comments 0
You need to be logged in to leave comments. Login now