Mercurial > p > roundup > code
comparison doc/design.txt @ 910:299f4890427d
documentation reorg post-new-security
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Mon, 29 Jul 2002 23:30:14 +0000 |
| parents | 38a74d1351c5 |
| children | 42924a2fcacf |
comparison
equal
deleted
inserted
replaced
| 909:ef9c759c243e | 910:299f4890427d |
|---|---|
| 248 operation. | 248 operation. |
| 249 | 249 |
| 250 Hyperdb Interface Specification | 250 Hyperdb Interface Specification |
| 251 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 251 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 252 | 252 |
| 253 TODO: replace the Interface Specifications with links to the pydoc | |
| 254 | |
| 253 The hyperdb module provides property objects to designate | 255 The hyperdb module provides property objects to designate |
| 254 the different kinds of properties. These objects are used when | 256 the different kinds of properties. These objects are used when |
| 255 specifying what properties belong in classes:: | 257 specifying what properties belong in classes:: |
| 256 | 258 |
| 257 class String: | 259 class String: |
| 292 Here is the interface provided by the hyperdatabase:: | 294 Here is the interface provided by the hyperdatabase:: |
| 293 | 295 |
| 294 class Database: | 296 class Database: |
| 295 """A database for storing records containing flexible data types.""" | 297 """A database for storing records containing flexible data types.""" |
| 296 | 298 |
| 297 def __init__(self, storagelocator, journaltag): | 299 def __init__(self, config, journaltag=None): |
| 298 """Open a hyperdatabase given a specifier to some storage. | 300 """Open a hyperdatabase given a specifier to some storage. |
| 299 | 301 |
| 302 The 'storagelocator' is obtained from config.DATABASE. | |
| 300 The meaning of 'storagelocator' depends on the particular | 303 The meaning of 'storagelocator' depends on the particular |
| 301 implementation of the hyperdatabase. It could be a file name, | 304 implementation of the hyperdatabase. It could be a file name, |
| 302 a directory path, a socket descriptor for a connection to a | 305 a directory path, a socket descriptor for a connection to a |
| 303 database over the network, etc. | 306 database over the network, etc. |
| 304 | 307 |
| 416 the nodes in this class, the matching node's id is returned; | 419 the nodes in this class, the matching node's id is returned; |
| 417 otherwise a KeyError is raised. | 420 otherwise a KeyError is raised. |
| 418 """ | 421 """ |
| 419 | 422 |
| 420 def find(self, propname, nodeid): | 423 def find(self, propname, nodeid): |
| 421 """Get the ids of nodes in this class which link to a given node. | 424 """Get the ids of nodes in this class which link to the given nodes. |
| 422 | 425 |
| 426 'propspec' consists of keyword args propname={nodeid:1,} | |
| 423 'propname' must be the name of a property in this class, or a | 427 'propname' must be the name of a property in this class, or a |
| 424 KeyError is raised. That property must be a Link or Multilink | 428 KeyError is raised. That property must be a Link or Multilink |
| 425 property, or a TypeError is raised. 'nodeid' must be the id of | 429 property, or a TypeError is raised. |
| 426 an existing node in the class linked to by the given property, | 430 |
| 427 or an IndexError is raised. | 431 Any node in this class whose 'propname' property links to any of the |
| 428 """ | 432 nodeids will be returned. Used by the full text indexing, which |
| 433 knows that "foo" occurs in msg1, msg3 and file7, so we have hits | |
| 434 on these issues: | |
| 435 | |
| 436 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) | |
| 437 """ | |
| 438 | |
| 439 def filter(self, search_matches, filterspec, sort, group): | |
| 440 ''' Return a list of the ids of the active nodes in this class that | |
| 441 match the 'filter' spec, sorted by the group spec and then the | |
| 442 sort spec. | |
| 443 ''' | |
| 429 | 444 |
| 430 def list(self): | 445 def list(self): |
| 431 """Return a list of the ids of the active nodes in this class.""" | 446 """Return a list of the ids of the active nodes in this class.""" |
| 432 | 447 |
| 433 def count(self): | 448 def count(self): |
| 450 objects, or a TypeError is raised. None of the keys in 'properties' | 465 objects, or a TypeError is raised. None of the keys in 'properties' |
| 451 may collide with the names of existing properties, or a ValueError | 466 may collide with the names of existing properties, or a ValueError |
| 452 is raised before any properties have been added. | 467 is raised before any properties have been added. |
| 453 """ | 468 """ |
| 454 | 469 |
| 455 TODO: additional methods | 470 def getnode(self, nodeid, cache=1): |
| 471 ''' Return a Node convenience wrapper for the node. | |
| 472 | |
| 473 'nodeid' must be the id of an existing node of this class or an | |
| 474 IndexError is raised. | |
| 475 | |
| 476 'cache' indicates whether the transaction cache should be queried | |
| 477 for the node. If the node has been modified and you need to | |
| 478 determine what its values prior to modification are, you need to | |
| 479 set cache=0. | |
| 480 ''' | |
| 481 | |
| 482 class Node: | |
| 483 ''' A convenience wrapper for the given node. It provides a mapping | |
| 484 interface to a single node's properties | |
| 485 ''' | |
| 456 | 486 |
| 457 Hyperdatabase Implementations | 487 Hyperdatabase Implementations |
| 458 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 488 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 459 | 489 |
| 460 Hyperdatabase implementations exist to create the interface described in the | 490 Hyperdatabase implementations exist to create the interface described in the |
| 461 `hyperdb interface specification`_ | 491 `hyperdb interface specification`_ |
| 462 over an existing storage mechanism. Examples are relational databases, | 492 over an existing storage mechanism. Examples are relational databases, |
| 463 \*dbm key-value databases, and so on. | 493 \*dbm key-value databases, and so on. |
| 464 | 494 |
| 465 TODO: finish | 495 Several implementations are provided - they belong in the roundup.backends |
| 496 package. | |
| 466 | 497 |
| 467 | 498 |
| 468 Application Example | 499 Application Example |
| 469 ~~~~~~~~~~~~~~~~~~~ | 500 ~~~~~~~~~~~~~~~~~~~ |
| 470 | 501 |
| 548 Some of the classes in the Roundup database are considered | 579 Some of the classes in the Roundup database are considered |
| 549 issue classes. | 580 issue classes. |
| 550 The Roundup database layer adds detectors and user nodes, | 581 The Roundup database layer adds detectors and user nodes, |
| 551 and on issues it provides mail spools, nosy lists, and superseders. | 582 and on issues it provides mail spools, nosy lists, and superseders. |
| 552 | 583 |
| 553 TODO: where functionality is implemented. | |
| 554 | |
| 555 Reserved Classes | 584 Reserved Classes |
| 556 ~~~~~~~~~~~~~~~~ | 585 ~~~~~~~~~~~~~~~~ |
| 557 | 586 |
| 558 Internal to this layer we reserve three special classes | 587 Internal to this layer we reserve three special classes |
| 559 of nodes that are not issues. | 588 of nodes that are not issues. |
| 560 | 589 |
| 561 Users | 590 Users |
| 562 """""""""""" | 591 """"" |
| 563 | 592 |
| 564 Users are stored in the hyperdatabase as nodes of | 593 Users are stored in the hyperdatabase as nodes of |
| 565 class "user". The "user" class has the definition:: | 594 class "user". The "user" class has the definition:: |
| 566 | 595 |
| 567 hyperdb.Class(db, "user", username=hyperdb.String(), | 596 hyperdb.Class(db, "user", username=hyperdb.String(), |
| 568 password=hyperdb.String(), | 597 password=hyperdb.String(), |
| 569 address=hyperdb.String()) | 598 address=hyperdb.String()) |
| 570 db.user.setkey("username") | 599 db.user.setkey("username") |
| 571 | 600 |
| 572 Messages | 601 Messages |
| 573 """"""""""""""" | 602 """""""" |
| 574 | 603 |
| 575 E-mail messages are represented by hyperdatabase nodes of class "msg". | 604 E-mail messages are represented by hyperdatabase nodes of class "msg". |
| 576 The actual text content of the messages is stored in separate files. | 605 The actual text content of the messages is stored in separate files. |
| 577 (There's no advantage to be gained by stuffing them into the | 606 (There's no advantage to be gained by stuffing them into the |
| 578 hyperdatabase, and if messages are stored in ordinary text files, | 607 hyperdatabase, and if messages are stored in ordinary text files, |
| 593 that are stored in the system). | 622 that are stored in the system). |
| 594 The "summary" property contains a summary of the message for display | 623 The "summary" property contains a summary of the message for display |
| 595 in a message index. | 624 in a message index. |
| 596 | 625 |
| 597 Files | 626 Files |
| 598 """""""""""" | 627 """"" |
| 599 | 628 |
| 600 Submitted files are represented by hyperdatabase | 629 Submitted files are represented by hyperdatabase |
| 601 nodes of class "file". Like e-mail messages, the file content | 630 nodes of class "file". Like e-mail messages, the file content |
| 602 is stored in files outside the database, | 631 is stored in files outside the database, |
| 603 named after the file node designator (e.g. "file17"). | 632 named after the file node designator (e.g. "file17"). |
| 729 | 758 |
| 730 Class(db, "issue", fixer=hyperdb.Multilink("user"), | 759 Class(db, "issue", fixer=hyperdb.Multilink("user"), |
| 731 topic=hyperdb.Multilink("keyword"), | 760 topic=hyperdb.Multilink("keyword"), |
| 732 priority=hyperdb.Link("priority"), | 761 priority=hyperdb.Link("priority"), |
| 733 status=hyperdb.Link("status")) | 762 status=hyperdb.Link("status")) |
| 734 | |
| 735 | 763 |
| 736 (The "order" property hasn't been explained yet. It | 764 (The "order" property hasn't been explained yet. It |
| 737 gets used by the Web user interface for sorting.) | 765 gets used by the Web user interface for sorting.) |
| 738 | 766 |
| 739 The above isn't as pretty-looking as the schema specification | 767 The above isn't as pretty-looking as the schema specification |
| 913 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 941 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 914 | 942 |
| 915 A single command, roundup, provides basic access to | 943 A single command, roundup, provides basic access to |
| 916 the hyperdatabase from the command line:: | 944 the hyperdatabase from the command line:: |
| 917 | 945 |
| 918 roundup get [-list] designator[, designator,...] propname | 946 roundup-admin help |
| 919 roundup set designator[, designator,...] propname=value ... | 947 roundup-admin get [-list] designator[, designator,...] propname |
| 920 roundup find [-list] classname propname=value ... | 948 roundup-admin set designator[, designator,...] propname=value ... |
| 921 | 949 roundup-admin find [-list] classname propname=value ... |
| 922 TODO: more stuff here | 950 |
| 951 See ``roundup-admin help commands`` for a complete list of commands. | |
| 923 | 952 |
| 924 Property values are represented as strings in command arguments | 953 Property values are represented as strings in command arguments |
| 925 and in the printed results: | 954 and in the printed results: |
| 926 | 955 |
| 927 - Strings are, well, strings. | 956 - Strings are, well, strings. |
| 1161 property using links that allow you to download files | 1190 property using links that allow you to download files |
| 1162 checklist for a Link or Multilink property, | 1191 checklist for a Link or Multilink property, |
| 1163 display checkboxes for the available choices to permit filtering | 1192 display checkboxes for the available choices to permit filtering |
| 1164 ========= ==================================================================== | 1193 ========= ==================================================================== |
| 1165 | 1194 |
| 1195 TODO: See the htmltemplate pydoc for a complete list of the functions | |
| 1196 | |
| 1166 | 1197 |
| 1167 Index Views | 1198 Index Views |
| 1168 ~~~~~~~~~~~ | 1199 ~~~~~~~~~~~ |
| 1169 | 1200 |
| 1170 An index view contains two sections: a filter section | 1201 An index view contains two sections: a filter section |
| 1179 An index view specifier looks like this (whitespace | 1210 An index view specifier looks like this (whitespace |
| 1180 has been added for clarity):: | 1211 has been added for clarity):: |
| 1181 | 1212 |
| 1182 /issue?status=unread,in-progress,resolved& | 1213 /issue?status=unread,in-progress,resolved& |
| 1183 topic=security,ui& | 1214 topic=security,ui& |
| 1184 :group=+priority& | 1215 :group=priority& |
| 1185 :sort=-activity& | 1216 :sort=-activity& |
| 1186 :filters=status,topic& | 1217 :filters=status,topic& |
| 1187 :columns=title,status,fixer | 1218 :columns=title,status,fixer |
| 1188 | 1219 |
| 1189 | 1220 |
| 1272 <td><display call="plain('fixer')"></td> | 1303 <td><display call="plain('fixer')"></td> |
| 1273 </property> | 1304 </property> |
| 1274 </tr> | 1305 </tr> |
| 1275 | 1306 |
| 1276 Sorting | 1307 Sorting |
| 1277 """""""""""""" | 1308 """"""" |
| 1278 | 1309 |
| 1279 String and Date values are sorted in the natural way. | 1310 String and Date values are sorted in the natural way. |
| 1280 Link properties are sorted according to the value of the | 1311 Link properties are sorted according to the value of the |
| 1281 "order" property on the linked nodes if it is present; or | 1312 "order" property on the linked nodes if it is present; or |
| 1282 otherwise on the key string of the linked nodes; or | 1313 otherwise on the key string of the linked nodes; or |
| 1363 The spool section lists messages in the issue's "messages" | 1394 The spool section lists messages in the issue's "messages" |
| 1364 property. The index of messages displays the "date", "author", | 1395 property. The index of messages displays the "date", "author", |
| 1365 and "summary" properties on the message nodes, and selecting a | 1396 and "summary" properties on the message nodes, and selecting a |
| 1366 message takes you to its content. | 1397 message takes you to its content. |
| 1367 | 1398 |
| 1399 Access Control | |
| 1400 -------------- | |
| 1401 | |
| 1402 At each point that requires an action to be performed, the security mechanisms | |
| 1403 are asked if the current user has permission. This permission is defined as a | |
| 1404 Permission. | |
| 1405 | |
| 1406 Individual assignment of Permission to user is unwieldy. The concept of a | |
| 1407 Role, which encompasses several Permissions and may be assigned to many Users, | |
| 1408 is quite well developed in many projects. Roundup will take this path, and | |
| 1409 allow the multiple assignment of Roles to Users, and multiple Permissions to | |
| 1410 Roles. These definitions are not persistent - they're defined when the | |
| 1411 application initialises. | |
| 1412 | |
| 1413 There will be two levels of Permission. The Class level permissions define | |
| 1414 logical permissions associated with all nodes of a particular class (or all | |
| 1415 classes). The Node level permissions define logical permissions associated | |
| 1416 with specific nodes by way of their user-linked properties. | |
| 1417 | |
| 1418 | |
| 1419 Access Control Interface Specification | |
| 1420 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1421 | |
| 1422 The security module defines:: | |
| 1423 | |
| 1424 class Permission: | |
| 1425 ''' Defines a Permission with the attributes | |
| 1426 - name | |
| 1427 - description | |
| 1428 - klass (optional) | |
| 1429 | |
| 1430 The klass may be unset, indicating that this permission is not | |
| 1431 locked to a particular hyperdb class. There may be multiple | |
| 1432 Permissions for the same name for different classes. | |
| 1433 ''' | |
| 1434 | |
| 1435 class Role: | |
| 1436 ''' Defines a Role with the attributes | |
| 1437 - name | |
| 1438 - description | |
| 1439 - permissions | |
| 1440 ''' | |
| 1441 | |
| 1442 class Security: | |
| 1443 def __init__(self, db): | |
| 1444 ''' Initialise the permission and role stores, and add in the | |
| 1445 base roles (for admin user). | |
| 1446 ''' | |
| 1447 | |
| 1448 def getPermission(self, permission, classname=None): | |
| 1449 ''' Find the Permission matching the name and for the class, if the | |
| 1450 classname is specified. | |
| 1451 | |
| 1452 Raise ValueError if there is no exact match. | |
| 1453 ''' | |
| 1454 | |
| 1455 def hasPermission(self, permission, userid, classname=None): | |
| 1456 ''' Look through all the Roles, and hence Permissions, and see if | |
| 1457 "permission" is there for the specified classname. | |
| 1458 ''' | |
| 1459 | |
| 1460 def hasNodePermission(self, classname, nodeid, **propspec): | |
| 1461 ''' Check the named properties of the given node to see if the | |
| 1462 userid appears in them. If it does, then the user is granted | |
| 1463 this permission check. | |
| 1464 | |
| 1465 'propspec' consists of a set of properties and values that | |
| 1466 must be present on the given node for access to be granted. | |
| 1467 | |
| 1468 If a property is a Link, the value must match the property | |
| 1469 value. If a property is a Multilink, the value must appear | |
| 1470 in the Multilink list. | |
| 1471 ''' | |
| 1472 | |
| 1473 def addPermission(self, **propspec): | |
| 1474 ''' Create a new Permission with the properties defined in | |
| 1475 'propspec' | |
| 1476 ''' | |
| 1477 | |
| 1478 def addRole(self, **propspec): | |
| 1479 ''' Create a new Role with the properties defined in 'propspec' | |
| 1480 ''' | |
| 1481 | |
| 1482 def addPermissionToRole(self, rolename, permission): | |
| 1483 ''' Add the permission to the role's permission list. | |
| 1484 | |
| 1485 'rolename' is the name of the role to add permission to. | |
| 1486 ''' | |
| 1487 | |
| 1488 Modules such as ``cgi_client.py`` and ``mailgw.py`` define their own | |
| 1489 permissions like so (this example is ``cgi_client.py``):: | |
| 1490 | |
| 1491 def initialiseSecurity(security): | |
| 1492 ''' Create some Permissions and Roles on the security object | |
| 1493 | |
| 1494 This function is directly invoked by security.Security.__init__() | |
| 1495 as a part of the Security object instantiation. | |
| 1496 ''' | |
| 1497 p = security.addPermission(name="Web Registration", | |
| 1498 description="Anonymous users may register through the web") | |
| 1499 security.addToRole('Anonymous', p) | |
| 1500 | |
| 1501 Detectors may also define roles in their init() function:: | |
| 1502 | |
| 1503 def init(db): | |
| 1504 # register an auditor that checks that a user has the "May Resolve" | |
| 1505 # Permission before allowing them to set an issue status to "resolved" | |
| 1506 db.issue.audit('set', checkresolvedok) | |
| 1507 p = db.security.addPermission(name="May Resolve", klass="issue") | |
| 1508 security.addToRole('Manager', p) | |
| 1509 | |
| 1510 The instance dbinit module then has in ``open()``:: | |
| 1511 | |
| 1512 # open the database - it must be modified to init the Security class | |
| 1513 # from security.py as db.security | |
| 1514 db = Database(instance_config, name) | |
| 1515 | |
| 1516 # add some extra permissions and associate them with roles | |
| 1517 ei = db.security.addPermission(name="Edit", klass="issue", | |
| 1518 description="User is allowed to edit issues") | |
| 1519 db.security.addPermissionToRole('User', ei) | |
| 1520 ai = db.security.addPermission(name="View", klass="issue", | |
| 1521 description="User is allowed to access issues") | |
| 1522 db.security.addPermissionToRole('User', ai) | |
| 1523 | |
| 1524 In the dbinit ``init()``:: | |
| 1525 | |
| 1526 # create the two default users | |
| 1527 user.create(username="admin", password=Password(adminpw), | |
| 1528 address=instance_config.ADMIN_EMAIL, roles='Admin') | |
| 1529 user.create(username="anonymous", roles='Anonymous') | |
| 1530 | |
| 1531 Then in the code that matters, calls to ``hasPermission`` and | |
| 1532 ``hasNodePermission`` are made to determine if the user has permission | |
| 1533 to perform some action:: | |
| 1534 | |
| 1535 if db.security.hasPermission('issue', 'Edit', userid): | |
| 1536 # all ok | |
| 1537 | |
| 1538 if db.security.hasNodePermission('issue', nodeid, assignedto=userid): | |
| 1539 # all ok | |
| 1540 | |
| 1541 Code in the core will make use of these methods, as should code in auditors in | |
| 1542 custom templates. The htmltemplate will implement a new tag, ``<require>`` | |
| 1543 which has the form:: | |
| 1544 | |
| 1545 <require permission="name,name,name" assignedto="$userid" status="open"> | |
| 1546 HTML to display if the user has the permission. | |
| 1547 <else> | |
| 1548 HTML to display if the user does not have the permission. | |
| 1549 </require> | |
| 1550 | |
| 1551 where: | |
| 1552 | |
| 1553 - the permission attribute gives a comma-separated list of permission names. | |
| 1554 These are checked in turn using ``hasPermission`` and requires one to | |
| 1555 be OK. | |
| 1556 - the other attributes are lookups on the node using ``hasNodePermission``. If | |
| 1557 the attribute value is "$userid" then the current user's userid is tested. | |
| 1558 | |
| 1559 Any of these tests must pass or the ``<require>`` check will fail. The section | |
| 1560 of html within the side of the ``<else>`` that fails is remove from processing. | |
| 1561 | |
| 1562 Authentication of Users | |
| 1563 ~~~~~~~~~~~~~~~~~~~~~~~ | |
| 1564 | |
| 1565 Users must be authenticated correctly for the above controls to work. This is | |
| 1566 not done in the current mail gateway at all. Use of digital signing of | |
| 1567 messages could alleviate this problem. | |
| 1568 | |
| 1569 The exact mechanism of registering the digital signature should be flexible, | |
| 1570 with perhaps a level of trust. Users who supply their signature through their | |
| 1571 first message into the tracker should be at a lower level of trust to those | |
| 1572 who supply their signature to an admin for submission to their user details. | |
| 1573 | |
| 1574 | |
| 1575 Anonymous Users | |
| 1576 ~~~~~~~~~~~~~~~ | |
| 1577 | |
| 1578 The "anonymous" user must always exist, and defines the access permissions for | |
| 1579 anonymous users. Unknown users accessing Roundup through the web or email | |
| 1580 interfaces will be logged in as the "anonymous" user. | |
| 1581 | |
| 1582 | |
| 1583 Use Cases | |
| 1584 ~~~~~~~~~ | |
| 1585 | |
| 1586 public - end users can submit bugs, request new features, request support | |
| 1587 The Users would be given the default "User" Role which gives "View" and | |
| 1588 "Edit" Permission to the "issue" class. | |
| 1589 developer - developers can fix bugs, implement new features, provide support | |
| 1590 A new Role "Developer" is created with the Permission "Fixer" which is | |
| 1591 checked for in custom auditors that see whether the issue is being | |
| 1592 resolved with a particular resolution ("fixed", "implemented", | |
| 1593 "supported") and allows that resolution only if the permission is | |
| 1594 available. | |
| 1595 manager - approvers/managers can approve new features and signoff bug fixes | |
| 1596 A new Role "Manager" is created with the Permission "Signoff" which is | |
| 1597 checked for in custom auditors that see whether the issue status is being | |
| 1598 changed similar to the developer example. | |
| 1599 admin - administrators can add users and set user's roles | |
| 1600 The existing Role "Admin" has the Permissions "Edit" for all classes | |
| 1601 (including "user") and "Web Roles" which allow the desired actions. | |
| 1602 system - automated request handlers running various report/escalation scripts | |
| 1603 A combination of existing and new Roles, Permissions and auditors could | |
| 1604 be used here. | |
| 1605 privacy - issues that are only visible to some users | |
| 1606 A new property is added to the issue which marks the user or group of | |
| 1607 users who are allowed to view and edit the issue. An auditor will check | |
| 1608 for edit access, and the htmltemplate <require> tag can check for view | |
| 1609 access. | |
| 1610 | |
| 1368 | 1611 |
| 1369 Deployment Scenarios | 1612 Deployment Scenarios |
| 1370 -------------------- | 1613 -------------------- |
| 1371 | 1614 |
| 1372 The design described above should be general enough | 1615 The design described above should be general enough |
