0.00%
Search · Index

Weblog Page

Filtered by date 2008-02-17, 71 - 80 of 111 Postings (all, summary)

OpenACS Permissions Tediously Explained

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

by Vadim Nasardinov. Modified and converted to Docbook XML by Roberto Mello

The code has been modified since this document was written so it is now out of date. See this forum thread.

Who (grantee_id) can do what (privilege) on which object (object_id).

The general permissions system has a flexible (and relatively complex) data model in OpenACS. Developers who have not had the time to learn the internals of the data model may end up writing seemingly correct code that crashes their system in weird ways. This writeup is the result of my running into such a piece of code and trying to understand exactly what went wrong. It is geared towards developers who understand the general permissions system to the extent that is described in the Groups, Context, Permissions documentation, but who have not had the opportunity to take a long, careful look at the system internals.

In OpenACS, most of the interesting tables are expected to extend (subtype) the acs_objects table, i.e. they are expected to have an integer primary key column that references the object_id column of acs_objects.

create table acs_objects (
      object_id             integer
          not null
          constraint acs_objects_pk primary key,
      object_type
          not null
          constraint acs_objects_object_type_fk references acs_object_types (object_type),
      context_id
          constraint acs_objects_context_id_fk references acs_objects(object_id),
      security_inherit_p	  char(1) default 't'
          not null,
      constraint acs_objects_sec_inherit_p_ck
          check (security_inherit_p in ('t', 'f')),
      creation_user         integer,
      creation_date         date default sysdate not null,
      creation_ip           varchar2(50),
      last_modified         date default sysdate not null,
      modifying_user        integer,
      modifying_ip          varchar2(50),
      constraint acs_objects_context_object_un
          unique (context_id, object_id) disable
);
    

This means that items that want to use the features of the OpenACS object system needs to have an entry in the acs_objects. This allows developers to define relationships between any two entities A and B by defining a relationship between their corresponding entries in the acs_objects table. One of the applications of this powerful capability is the general permissions system.

At the heart of the permission system are two tables: acs_privileges and acs_permissions.

  create table acs_privileges (
      privilege           varchar2(100) not null
          constraint acs_privileges_pk primary key,
      pretty_name         varchar2(100),
      pretty_plural       varchar2(100)
  );
    
  create table acs_permissions (
      object_id
          not null
          constraint acs_permissions_on_what_id_fk references acs_objects (object_id),
      grantee_id
          not null
          constraint acs_permissions_grantee_id_fk references parties (party_id),
      privilege
          not null
          constraint acs_permissions_priv_fk references acs_privileges (privilege),
      constraint acs_permissions_pk
          primary key (object_id, grantee_id, privilege)
  );
    

The acs_privileges table stores named privileges like read, write, delete, create, and admin. The acs_permissions table stores assertions of the form:

Who (grantee_id) can do what (privilege) on which object (object_id).

The micromanaging approach to system security would be to require application developers to store permission information explicitly about every object, i.e. if the system has 100,000 and 1,000 users who have the read privilege on all objects, then we would need to store 100,000,000 entries of the form:

object_id grantee_id privilege
object_id_1 user_id_1 'read'
object_id_1 user_id_2 'read'
...
object_id_1 user_id_n 'read'
object_id_2 user_id_1 'read'
object_id_2 user_id_2 'read'
...
object_id_2 user_id_n 'read'
...
...
object_id_m user_id_1 'read'
object_id_m user_id_2 'read'
...
object_id_m user_id_n 'read'

Although quite feasible, this approach fails to take advantage of the fact that objects in the system are commonly organized hierarchally, and permissions usually follow the hierarchical structure, so that if user X has the read privilege on object A, she typically also has the read privilege on all objects attached under A.

The general permission system takes advantage of the hierarchical organization of objects to unburden developers of the necessity to explicitly maintain security information for every single object. There are three kinds of hierarchies involved. These are discussed in the following sections.

Suppose objects A, B, ..., and F form the following hierarchy.

Table11.2.Context Hierarchy Example

A

object_id=10

B

object_id=20

C

object_id=30

D

object_id=40

E

object_id=50

F

object_id=60


This can be represented in the acs_objects table by the following entries:

Table11.3.acs_objects example data

object_id context_id
20 10
30 10
40 20
50 20
60 30

The first entry tells us that object 20 is the descendant of object 10, and the third entry shows that object 40 is the descendant of object 20. By running a CONNECT BY query, we can compute that object 40 is the second-generation descendant of object 10. With this in mind, if we want to record the fact that user Joe has the read privilege on objects A, ..., F, we only need to record one entry in the acs_permissions table.

object grantee privilege
A Joe read

The fact that Joe can also read B, C, ..., and F can be derived by ascertaining that these objects are children of A by traversing the context hierarchy. As it turns out, hierarchical queries are expensive. As Rafael Schloming put it so aptly, Oracle can't deal with hierarchies for shit.

One way to solve this problem is to cache a flattened view of the context tree like so:

object ancestor n_generations
A A 0
B B 0
B A 1
C C 0
C A 1
D D 0
D B 1
D A 2
E E 0
E B 1
E A 2
F F 0
F C 1
F A 2

Note that the number of entries in the flattened view grows exponentially with respect to the depth of the context tree. For instance, if you have a fully populated binary tree with a depth of n, then the number of entries in its flattened view is

1 + 2*2 + 3*4 + 4*8 + 5*16 + ... + (n+1)*2n = n*2n+1 + 1

Despite its potentially great storage costs, maintaining a flattened representation of the context tree is exactly what OpenACS does. The flattened context tree is stored in the acs_object_context_index table.

  create table acs_object_context_index (
      object_id
          not null
          constraint acs_obj_context_idx_obj_id_fk references acs_objects (object_id),
      ancestor_id
          not null
          constraint acs_obj_context_idx_anc_id_fk references acs_objects (object_id),
      n_generations	    integer
          not null
          constraint acs_obj_context_idx_n_gen_ck check (n_generations >= 0),
      constraint acs_object_context_index_pk
          primary key (object_id, ancestor_id)
  ) organization index;
    

A few things to note about this table are these. Number one, it is an index-organized table, which means it is substantially optimized for access by primary key. Number two, as the above computations suggest, the size of the table grows polynomially with respect to the average number of descendants that an object has, and exponentially with respect to the depth of the context tree.

The acs_object_context_index is kept in sync with the acs_objects table by triggers like this:

create or replace trigger acs_objects_context_id_in_tr
after insert on acs_objects
for each row
begin
    insert into acs_object_context_index
     (object_id, ancestor_id, n_generations)
    values
     (:new.object_id, :new.object_id, 0);

    if :new.context_id is not null and :new.security_inherit_p = 't' then
      insert into acs_object_context_index
       (object_id, ancestor_id,
        n_generations)
      select
       :new.object_id as object_id, ancestor_id,
       n_generations + 1 as n_generations
      from acs_object_context_index
      where object_id = :new.context_id;
    elsif :new.object_id != 0 then
      -- 0 is the id of the security context root object
      insert into acs_object_context_index
       (object_id, ancestor_id, n_generations)
      values
       (:new.object_id, 0, 1);
    end if;
end;

One final note about acs_objects. By setting an object's security_inherit_p column to 'f', you can stop permissions from cascading down the context tree. In the following example, Joe does not have the read permissions on C and F.


A
object_id=10
readablebyJoe


B
object_id=20
readablebyJoe


C
object_id=30
security_inherit_p='f'
notreadablebyJoe


D
object_id=40


E
object_id=50


F
object_id=60
security_inherit_p='f'
notreadablebyJoe

Privileges are also organized hierarchically. In addition to the five main system privileges defined in the ACS Kernel data model, application developers may define their own. Note, however, that this is no longer recommended practice.

By defining parent-child relationship between privileges, the OpenACS data model makes it easier for developers to manage permissions. Instead of granting a user explicit read, write, delete, and create privileges on an object, it is sufficient to grant the user the admin privilege to which the first four privileges are tied. Privileges are structured as follows.

admin
create delete read write

Note that admin privileges are greater than read, write, create and delete privileges combined. Issuing someone read, write, create and delete privileges will not result in the person getting admin privileges.

The parent-child relationship between privileges is represented in the acs_privilege_hierarchy table:

  create table acs_privilege_hierarchy (
      privilege
          not null
          constraint acs_priv_hier_priv_fk references acs_privileges (privilege),
      child_privilege
          not null
          constraint acs_priv_hier_child_priv_fk references acs_privileges (privilege),
      constraint acs_privilege_hierarchy_pk
          primary key (privilege, child_privilege)
  );
    

As in the case of the context hierarchy, it is convenient to have a flattened representation of this hierarchal structure. This is accomplished by defining the following view.

  create or replace view acs_privilege_descendant_map
  as
  select
    p1.privilege,
    p2.privilege as descendant
  from
    acs_privileges p1,
    acs_privileges p2
  where
    p2.privilege in
      (select
         child_privilege
       from
         acs_privilege_hierarchy
       start with
         privilege = p1.privilege
       connect by
         prior child_privilege = privilege
      )
    or p2.privilege = p1.privilege;
    

As the number of different privileges in the system is expected to be reasonably small, there is no pressing need to cache the flattened ansector-descendant view of the privilege hierarchy in a specially maintained table like it is done in the case of the context hierarchy.

Now for the third hierarchy playing a promiment role in the permission system. The party data model is set up as follows.

  create table parties (
      party_id
          not null
          constraint parties_party_id_fk references acs_objects (object_id)
          constraint parties_party_id_pk primary key,
      email               varchar2(100)
          constraint parties_email_un unique,
      url                 varchar2(200)
  );
    
  create table persons (
      person_id
          not null
          constraint persons_person_id_fk references parties (party_id)
          constraint persons_person_id_pk primary key,
      first_names          varchar2(100)
          not null,
      last_name            varchar2(100)
          not null
  );
    
  create table users (
      user_id
          not null
          constraint users_user_id_fk references persons (person_id)
          constraint users_user_id_pk primary key,
      password        char(40),
      -- other attributes
  );
    
  create table groups (
      group_id
          not null
          constraint groups_group_id_fk references parties (party_id)
          constraint groups_group_id_pk primary key,
      group_name           varchar2(100) not null
  );
    

Recall that the grantee_id column of the acs_permissions table references parties.party_id. This means that you can grant a privilege on an object to a party, person, user, or group. Groups represent aggregations of parties. The most common scenario that you are likely to encounter is a group that is a collection of users, although you could also have collections of persons, groups, parties, or any mix thereof.

Given that the most common use of groups is to partition users, how do you build groups? One way is to grant membership explicitly. If you have a group named Pranksters, you can assign membership to Pete, Poly, and Penelope. The fact that these users are members of the Pranksters group will be recorded in the membership_rels and acs_rels tables:

  create table acs_rels (
      rel_id
          not null
          constraint acs_rels_rel_id_fk references acs_objects (object_id)
          constraint acs_rels_pk primary key,
      rel_type
          not null
          constraint acs_rels_rel_type_fk references acs_rel_types (rel_type),
      object_id_one
          not null
          constraint acs_object_rels_one_fk references acs_objects (object_id),
      object_id_two
          not null
          constraint acs_object_rels_two_fk references acs_objects (object_id),
      constraint acs_object_rels_un
          unique (rel_type, object_id_one, object_id_two)
  );
    
  create table membership_rels (
      rel_id
          constraint membership_rel_rel_id_fk references acs_rels (rel_id)
          constraint membership_rel_rel_id_pk primary key,
      -- null means waiting for admin approval
      member_state         varchar2(20)
          constraint membership_rel_mem_ck
           check (member_state in ('approved', 'banned', 'rejected', 'deleted'))
  );
    

The acs_rels table entries would look like so:

rel_type object_one object_two
membership_rel Pranksters Pete
membership_rel Pranksters Poly
membership_rel Pranksters Penelope

Read acs_rels: right-side is a subset of left-side, ie object2 is a part of object1.

Another way of building up groups is by adding subgroups. Suppose we define Merry Pranksters and Sad Pranksters as subgroups of Pranksters. We say that the Pranksters group is composed of groups Merry Pranksters and Sad Pranksters. This information is stored in the acs_rels and composition_rels tables.

create table composition_rels (
    rel_id
        constraint composition_rel_rel_id_fk references acs_rels (rel_id)
        constraint composition_rel_rel_id_pk primary key
);
    

The relevant entries in the acs_rels look like so.

rel_type object_one object_two
composition_rel Pranksters Merry Pranksters
composition_rel Pranksters Sad Pranksters

The composition relationship means that if I add Matt, Mel, and Mary to the Merry Pranksters, they should also automatically become members of the Pranksters group. The situation we are facing in trying to determine whether or not a user is member of a group is similar to the one discussed above in the case of the context hierarchy. Groups can form hierarchies with respect to the composition relationship. The compositon relationship is transitive. If G1 is a subgroup of G2, and G2 is a subgroup of G3, then G1 is a subgroup of G3; that is, any member of G1 is also a member of G3.

Traversing the group composition hierarchy requires running hierarchical queries, which are expensive in Oracle. As we saw in the Context Hierarchy section, one way of reducing the performance hit incurred by hierarchical queries is to cache query results in a table maintained by triggers. The OpenACS data model defines two such tables:

 create table group_component_index (
          group_id        not null
                          constraint group_comp_index_group_id_fk
                          references groups (group_id),
          component_id    not null
                          constraint group_comp_index_comp_id_fk
                          references groups (group_id),
          rel_id          not null
                          constraint group_comp_index_rel_id_fk
                          references composition_rels (rel_id),
          container_id    not null
                          constraint group_comp_index_cont_id_ck
                          references groups (group_id),
          constraint group_component_index_ck
          check (group_id != component_id),
          constraint group_component_index_pk
          primary key (group_id, component_id, rel_id)
  ) organization index;
    
  create table group_member_index (
      group_id
          not null
          constraint group_member_index_grp_id_fk references groups (group_id),
      member_id
          not null
          constraint group_member_index_mem_id_fk references parties (party_id),
      rel_id
          not null
          constraint group_member_index_rel_id_fk references membership_rels (rel_id),
      container_id
          not null
          constraint group_member_index_cont_id_fk references groups (group_id),
      constraint group_member_index_pk
          primary key (member_id, group_id, rel_id)
  ) organization index;
    

The group_component_index table stores a flattened representation of the group composition hierarchy that is maintained in sync with the acs_rels and composition_rels tables through triggers.

additional comments

As far as the group_member_index table goes, I am not sure I understand its purpose. It maintains group-member relationships that are resolved with respect to group composition. Note that information stored in group_member_index can be trivially derived by joining membership_rels, acs_rels, and group_component_index. Here is a view that does it. (This view is not part of the OpenACS Kernel data model.)

create or replace view group_member_view
as
select
  gci.group_id, r.object_id_two as member_id
from
  (
   select
     group_id, group_id as component_id
   from
     groups
   union
   select
     group_id, component_id
   from
     group_component_index
  ) gci,
  membership_rels mr,
  acs_rels r
where
  mr.rel_id = r.rel_id
  and r.object_id_one = gci.component_id;
    

A heuristic way to verify that group_member_view is essentially identical to group_member_index is to compute the symmetric difference between the two:

select
  group_id, member_id
from
  (
   select group_id, member_id from group_member_view
   minus
   select group_id, member_id from group_member_index
  )
union
select
  group_id, member_id
from
  (
   select group_id, member_id from group_member_index
   minus
   select group_id, member_id from group_member_view
  )
    

The query returns no rows. The important point is, if we have a flattened view of the composition hierarchy -- like one provided by the group_component_index table -- membership relationship resolution can be computed trivially with no hierarchical queries involved. There is no need to keep the view in a denormalized table, unless doing so results in substantial performance gains.

Security information is queried by calling the acs_permission.permission_p function in OpenACS. This is accessible from Tcl via the permission::permission_p procedure.

  create or replace package body acs_permission
  as
    -- some stuff removed for the sake of brevity

    function permission_p (
      object_id	 acs_objects.object_id%TYPE,
      party_id	 parties.party_id%TYPE,
      privilege	 acs_privileges.privilege%TYPE
    ) return char
    as
      exists_p char(1);
    begin
      -- XXX This must be fixed: -1 shouldn't be hardcoded (it is the public)
      select decode(count(*),0,'f','t') into exists_p
        from acs_object_party_privilege_map
       where object_id = permission_p.object_id
         and party_id in (permission_p.party_id, -1)
         and privilege = permission_p.privilege;
      return exists_p;
    end;

  end acs_permission;
    

problem avoidance

The function queries acs_object_party_privilege_map, which is a humongous view that joins three flattened hierarchies: the context tree, the privilege hierarchy, the party composition (and membership) hierarchy. It contains an extremely large number of rows. About the only kind of query you can run against it is the one performed by the acs_permission.permission_p function. Anything other than that would take forever to finish or would ultimately result in a query error.

For example, do not try to do things like

select count(*)
  from acs_object_party_privilege_map;
    

To give another example of things to avoid, I have seen code like this:

  declare
      cursor cur is
        select
           object_id, party_id
        from
           acs_object_party_privilege_map
        where
           privilege = 'foo_create';
  begin
      -- revoke all 'foo_create' permissions
      for rec in cur
      loop
          acs_permission.revoke_permission (
              object_id  => rec.object_id,
              grantee_id => rec.party_id,
              privilege  => 'foo_create'
          );
      end loop;

      acs_privilege.remove_child('admin','foo_create');
      acs_privilege.drop_privilege('foo');

  end;
  /
    

The acs_permission.revoke_permission function merely runs a delete statement like so:

  delete from
     acs_permissions
  where
     object_id = revoke_permission.object_id
     and grantee_id = revoke_permission.grantee_id
     and privilege = revoke_permission.privilege;
    

Note that in the above example, acs_permissions had only one entry that needed to be deleted:

object_id grantee_id privilege
default_context registered_users foo_create

The above script would never get around to deleting this entry because it had to loop through a gazillion rows in the humongous acs_object_party_privilege_map view.

create or replace view acs_object_party_privilege_map
as
select
  ogpm.object_id,
  gmm.member_id as party_id,
  ogpm.privilege
from
  acs_object_grantee_priv_map ogpm,
  group_member_map gmm
where
  ogpm.grantee_id = gmm.group_id
union
select
  object_id,
  grantee_id as party_id,
  privilege
from
  acs_object_grantee_priv_map;
    
create or replace view acs_object_grantee_priv_map
as
select
  a.object_id,
  a.grantee_id,
  m.descendant as privilege
from
  acs_permission_all a,
  acs_privilege_descendant_map m
where
  a.privilege = m.privilege;
    
create or replace view acs_permissions_all
as
select
  op.object_id,
  p.grantee_id,
  p.privilege
from
  acs_object_paths op,
  acs_permissions p
where
  op.ancestor_id = p.object_id;
    
create or replace view acs_object_paths
as
select
  object_id,
  ancestor_id,
  n_generations
from
  acs_object_context_index;
    

create or replace view group_member_map
as
select
  group_id,
  member_id,
  rel_id,
  container_id
from
  group_member_index;
    

Enabling WYSIWYG

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

by Nima Mazloumi

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

Most of the forms in OpenACS are created using the form builder, see the section called “Using Form Builder: building html forms dynamically”. For detailed information on the API take a look here.

The following section shows how you can modify your form to allow WYSIWYG functionalities.

Convert your page to use ad_form (some changes but worth it)

Here an examples. From:

	template::form create my_form
	template::element create my_form my_form_id -label "The ID" -datatype integer -widget hidden
	template::element create my_form my_input_field_1 -html { size 30 } -label "Label 1" -datatype text -optional
	template::element create my_form my_input_field_2 -label "Label 2" -datatype text -help_text "Some Help" -after_html {<a name="#">Anchor</a>}
	

To:

	ad_form -name my_form -form {
		my_form_id:key(acs_object_id_seq)
 		{my_input_field_1:text,optional
               {label "Label 1"}
               {html {size 30}}}
      	{my_input_field_2:text
               {label "Label 2"}
               {help_text "Some Help"}
	       	   {after_html
               {<a name="#">Anchor</a>}}}
	} ...
	

Warning

You must not give your your form the same name that your page has. Otherwise HTMLArea won't load.

Convert your textarea widget to a richtext widget and enable htmlarea.

The htmlarea_p-flag can be used to prevent WYSIWYG functionality. Defaults to true if left away.

From:

	{my_input_field_2:text
	

To:

	{my_input_field_2:richtext(richtext)
			{htmlarea_p "t"}
	

The richtext widget presents a list with two elements: text and content type. To learn more on existing content types search in Google for "MIME-TYPES" or take a look at the cr_mime_types table.

Make sure that both values are passed as a list to your ad_form or you will have problems displaying the content or handling the data manipulation correctly.

Depending on the data model of your package you either support a content format or don't. If you don't you can assume "text/html" or "text/richtext" or "text/enhanced".

The relevant parts in your ad_form definition are the switches -new_data, -edit_data, -on_request and -on_submit.

To allow your data to display correctly you need to add an -on_request block. If you have the format stored in the database pass this as well else use "text/html":

	set my_input_field_2 [template::util::richtext::create $my_input_field_2 "text/html"]
	

Now make sure that your SQL queries that do the data manipulation retrieve the correct value. If you simply use my_input_field_2 you will store a list. Thus you need to add an -on_submit block:

	set my_input_field_2 [ template::util::richtext::get_property contents $my_input_field_2]
	set format [ template::util::richtext::get_property format $my_input_field_2] #This is optional
	

Now the correct values for my_input_field_2 and format are passed to the -new_data and -edit_data blocks which don't need to get touched.

To make HTMLArea optional per package instance define a string parameter UseWysiwygP which defaults 0 for your package using the APM.

In your edit page make the following changes

	# Is WYSIWYG enabled?
	set use_wysiwyg_p [parameter::get -parameter "UseWysiwygP" -default "f"]
	
	...
	
	{htmlarea_p $use_wysiwyg_p}
	

The -on_request switch should set this value for your form.

	set htmlarea_p $use_wysiwyg_p
	

All you need now is a configuration page where the user can change this setting. Create a configure.tcl file:

	ad_page_contract {

    	This page allows a faq admin to change the UseWysiwygP setting

	} {
    	{return_url ""}
	}

	set title "Should we support WYSIWYG?"
	set context [list $title]

	set use_wysiwyg_p

	ad_form -name categories_mode -form {
    	{enabled_p:text(radio)
        	{label "Enable WYSIWYG"}
        	{options {{Yes t} {No f}}}
        	{value $use_wysiwyg_p}
    	}
    	{return_url:text(hidden) {value $return_url}}
    	{submit:text(submit) {label "Change"}}
	} -on_submit {
    	parameter::set_value  -parameter "UseWysiwygP" -value $enabled_p
    	if {![empty_string_p $return_url]} {
        	ns_returnredirect $return_url
    	}
	}
	

In the corresponding ADP file write

	<master>
	<property name="title">@title@</property>
	<property name="context">@context@</property>

	<formtemplate id="categories_mode"></formtemplate>
	

And finally reference this page from your admin page

	#TCL:
	set return_url [ad_conn url]

	#ADP:
	<a href=configure?<%=[export_url_vars return_url]%>>Configure</a>
	

Initialize CVS (OPTIONAL)

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

CVS is a source control system. Create and initialize a directory for a local cvs repository.

[root tmp]# mkdir /cvsroot
[root tmp]# cvs -d /cvsroot init
[root tmp]#
mkdir /cvsroot
cvs -d /cvsroot init

Running multiple services on one machine

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

Services on different ports.To run a different service on another port but the same ip, simply repeat Install OpenACS 5.2.3rc1 replacing $OPENACS_SERVICE_NAME, and change the

set httpport              8000
set httpsport             8443 

to different values.

Services on different host names.For example, suppose you want to support http://service0.com and http://bar.com on the same machine. The easiest way is to assign each one a different ip address. Then you can install two services as above, but with different values for

set hostname               [ns_info hostname]
set address                127.0.0.1 

If you want to install two services with different host names sharing the same ip, you'll need nsvhr to redirect requests based on the contents of the tcp headers. See AOLserver Virtual Hosting with TCP by markd.

Upgrading the OpenACS files

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

OpenACS is distributed in many different ways:

  • as a collection of files

  • as one big tarball

  • via CVS

  • via automatic download from within the APM (package manager)

Upgrades work by first changing the file system (via any of the previous methods), and then using the APM to scan the file system, find upgrade scripts, and execute them. Starting with OpenACS 5.0, the last method was added, which automatically changes the file system for you. If you are using the last method, you can skip this page. This page describes whether or not you need to be upgrading using this page or not: the section called “Upgrading an OpenACS 5.0.0 or greater installation”

  • Upgrading files for a site which is not in a CVS repository.Unpack the tarball into a new directory and copy its contents on top of your working directory. Or just 'install software', select remote repository, and upgrade your files from there.

    [root root]# su - $OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME aolserver]$ cd /var/lib/aolserver
    [$OPENACS_SERVICE_NAME web]$ tar xzf /var/tmp/openacs-5-1.tar.gz
    [$OPENACS_SERVICE_NAME web]$ cp -r openacs-5-1/* openacs-4
    [$OPENACS_SERVICE_NAME openacs-upgrade]$ exit
    [root root]#
    su - $OPENACS_SERVICE_NAME
    cd /var/lib/aolserver
    tar xzf /var/tmp/openacs-5-1.tgz
    cp -r openacs-5-1/* openacs-4
    exit
    
  • Upgrading files for a site in a private CVS repository

    Many OpenACS site developers operate their own CVS repository to keep track of local customizations. In this section, we describe how to upgrade your local CVS repository with the latest OpenACS version, without overriding your own local customizations.

    This diagram explains the basic idea. However, the labels are incorrect. Step 1(a) has been removed, and Step 1(b) should be labelled Step 1.

    Figure5.2.Upgrading a local CVS repository

    Upgrading a local CVS repository

    • Step 0: Set up a working CVS checkout.To get your OpenACS code into your local CVS repository, you will set up a working CVS checkout of OpenACS. When you want to update your site, you'll update the working CVS checkout, import those changes into your local CVS checkout, create a temporary CVS checkout to merge your local changes, fix any conflicts, commit your changes, and then update your site. It sounds complicated, but it's not too bad, and it is the best way to work around CVS's limitations.

      This part describes how to set up your working CVS checkout. Once it is set up, you'll be able to update any packages using the existing working CVS checkout. We use one dedicated directory for each branch of OpenACS - if you are using OpenACS 5.1,x, you will need a 5.1 checkout. That will be good for 5.1, 5.11, 5.12, and so on. But when you want to upgrade to OpenACS 5.2, you'll need to check out another branch.

      The openacs-5-1-compat tag identifies the latest released version of OpenACS 5.1 (ie, 5.1.3 or 5.1.4) and the latest compatible version of each package. Each minor release of OpenACS since 5.0 has this tagging structure. For example, OpenACS 5.1.x has openacs-5-1-compat.

      You will want to separately check out all the packages you are using.

      [root root]# su - $OPENACS_SERVICE_NAME
      
      [$OPENACS_SERVICE_NAME aolserver]$ cd /var/lib/aolserver
      [$OPENACS_SERVICE_NAME aolserver]$ cvs -d :pserver:anonymous@cvs.openacs.org:/cvsroot checkout -r openacs-5-1-compat acs-core
      [$OPENACS_SERVICE_NAME aolserver]$ cd openacs-4/packages
      [$OPENACS_SERVICE_NAME aolserver]$ cvs -d :pserver:anonymous@cvs.openacs.org:/cvsroot checkout -r openacs-5-1-compatpackagename packagename2...
      
      [$OPENACS_SERVICE_NAME aolserver]$ cd ../..
      [$OPENACS_SERVICE_NAME aolserver]$ mv openacs-4 openacs-5-1
      
      

      Make sure your working CVS checkout doesn't have the entire CVS tree from OpenACS. A good way to check this is if it has a contrib directory. If it does, you probably checked out the entire tree. You might want to start over, remove your working CVS checkout, and try again.

    • Step 1: Import new OpenACS code.

      • Update CVS.Update your local CVS working checkout (unless you just set it up).

        [root root]# su - $OPENACS_SERVICE_NAME
        
        [$OPENACS_SERVICE_NAME aolserver]$ cd /var/lib/aolserver/openacs-5-1
        
        [$OPENACS_SERVICE_NAME aolserver]$ cvs up -Pd ChangeLog *.txt bin etc tcl www packages/*
        
      • Update a single package via cvs working checkout.You can add or upgrade a single package at a time, if you already have a cvs working directory.

        [root root]# su - $OPENACS_SERVICE_NAME
        
        [$OPENACS_SERVICE_NAME aolserver]$ cd /var/lib/aolserver/packages/openacs-5-1
        
        [$OPENACS_SERVICE_NAME openacs-5-1]$ cvs up -Pd packagename
        
        

        In the next section, the import must be tailored to just this package.

    • Step 2: Merge New OpenACS code.Now that you have a local copy of the new OpenACS code, you need to import it into your local CVS repository and resolve any conflicts that occur.

      Import the new files into your cvs repository; where they match existing files, they will become the new version of the file.

      [$OPENACS_SERVICE_NAME openacs-5-1]$  cd /var/lib/aolserver/openacs-5-1
      
      [$OPENACS_SERVICE_NAME openacs-5-1]$  cvs -d /var/lib/cvs import -m "upgrade to OpenACS 5.1" $OPENACS_SERVICE_NAME OpenACS openacs-5-1
      
      

      Tip

      If adding or upgrading a single package, run the cvs import from within the base directory of that package, and adjust the cvs command accordingly. In this example, we are adding the myfirstpackage package.

      [root root]# su - $OPENACS_SERVICE_NAME
      
      [$OPENACS_SERVICE_NAME aolserver]$ cd /var/lib/aolserver/openacs-5-0/package/myfirstpackage
      
      [$OPENACS_SERVICE_NAME myfirstpackage]$ cvs -d /var/lib/cvs/ import -m "importing package" $OPENACS_SERVICE_NAME/packages/myfirstpackage OpenACS openacs-5-1
      

      Create a new directory as temporary working space to reconcile conflicts between the new files and your current work. The example uses the cvs keyword yesterday, making the assumption that you haven't checked in new code to your local tree in the last day. This section should be improved to use tags instead of the keyword yesterday!

      [$OPENACS_SERVICE_NAME openacs-5.1]$  cd /var/lib/aolserver
      [$OPENACS_SERVICE_NAME tmp]$ rm -rf $OPENACS_SERVICE_NAME-upgrade
      [$OPENACS_SERVICE_NAME tmp]$ mkdir $OPENACS_SERVICE_NAME-upgrade
      [$OPENACS_SERVICE_NAME tmp]$ cvs checkout -d $OPENACS_SERVICE_NAME-upgrade -jOpenACS:yesterday -jOpenACS -kk $OPENACS_SERVICE_NAME > cvs.txt 2>&1
      (CVS feedback here)

      The file /var/tmp/openacs-upgrade/cvs.txt contains the results of the upgrade. If you changed files that are part of the OpenACS tarball and those changes conflict, you'll have to manually reconcile them. Use the emacs command M-x sort-lines (you may have to click Ctrl-space at the beginning of the file, and go to the end, and then try M-x sort-lines) and then, for each line that starts with a C, open that file and manually resolve the conflict by deleting the excess lines. When you're finished, or if there aren't any conflicts, save and exit.

      Once you've fixed any conflicts, commit the new code to your local tree.

      [$OPENACS_SERVICE_NAME tmp]$ cd $OPENACS_SERVICE_NAME-upgrade
      [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME-upgrade]$ cvs commit -m "Upgraded to 5.1"
      
    • Step 3: Upgrade your local staging site.Update your working tree with the new files. The CVS flags ensure that new directories are created and pruned directories destroyed.

      [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME-upgrade]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME
      
      [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ cvs up -Pd
      (CVS feedback)
      [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ exit
      [root root]# 

Upgrading files for a site using the OpenACS CVS repository (cvs.openacs.org)

  1. [$OPENACS_SERVICE_NAME ~]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ cvs up -Pd
    (CVS feedback)
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$

If you are upgrading a production OpenACS site which is on a private CVS tree, this process lets you do the upgrade without risking extended downtime or an unusable site:

  1. Declare a freeze on new cvs updates - ie, you cannot run cvs update on the production site

  2. Make a manual backup of the production site in addition to the automated backups

  3. Import the new code (for example, OpenACS 5.0.4, openacs-5-0-compat versions of ETP, blogger, and other applications) into a "vendor branch" of the $OPENACS_SERVICE_NAME CVS tree, as described in "Upgrading a local CVS repository", step 1, above. As soon as we do this, any cvs update command on production might bring new code onto the production site, which would be bad.

    Do step 2 above (merging conflicts in a $OPENACS_SERVICE_NAME-upgrade working tree).

  4. Manually resolve any conflicts in the working upgrade tree

  5. Use the upgrade script and a recent backup of the production database, to ake a new upgraded database called $OPENACS_SERVICE_NAME-upgrade. Now we have a new website called $OPENACS_SERVICE_NAME-upgrade.

  6. Test the $OPENACS_SERVICE_NAME-upgrade site

  7. If $OPENACS_SERVICE_NAME-upgrade is fully functional, do the real upgrade.

    1. Take down the $OPENACS_SERVICE_NAME site and put up a "down for maintenance" page.

    2. Repeat the upgrade with the most recent database

    3. Test the that the new site is functional. If so, change the upgraded site to respond to yourserver.net requests. If not, bring the original production site back up and return to the merge.

Bootstrapping OpenACS

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Jon Salz

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.
  • Tcl code: /tcl/0-acs-init.tcl and /packages/acs-kernel/bootstrap.tcl

This document describes the startup (bootstrapping) process for an AOLserver running OpenACS.

Before OpenACS 3.3, the OpenACS startup process was extremely simple: after AOLserver performed its internal initialization (reading the configuration file, loading shared libraries and module code, etc.) it scanned through the Tcl library directory (generally /var/lib/aolserver/yourservername/tcl), sourcing each file in sequence.

While this overall structure for initialization is still intact, package management has thrown a wrench into the works - there are a few extra things to do during initialization, most notably:

  • Examine the OpenACS file tree for files that should not be present in OpenACS (i.e., that were once part of the OpenACS distribution but have since been removed).

  • Scan the /packages directory for new packages.

  • Initialize enabled packages by sourcing their *-procs.tcl and *-init.tcl files.

This document examines in detail each of the steps involved in AOLserver/OpenACS startup.

As soon as the nsd daemon is executed by the init process (or otherwise), AOLserver reads its configuration file and chroots itself if necessary. It then loads shared libraries indicated in the .ini file (e.g., the Oracle driver and nssock), and sources Tcl module files (generally in /home/aol30/modules/tcl). This step is, and has always been, the same for all AOLservers, regardless of whether they are running OpenACS.

Next AOLserver sources, in lexicographical order, each file in the /tcl directory. The first such file is 0-acs-init.tcl, which doesn't do much directly except to determine the OpenACS path root (e.g., /var/lib/aolserver/yourservername) by trimming the final component from the path to the Tcl library directory (/var/lib/aolserver/yourservername/tcl). But 0-acs-init.tcl's has an important function, namely sourcing /packages/acs-core/bootstrap.tcl, which does the following:

  1. Initialize some NSVs used by the core. These NSVs are documented in /packages/acs-core/apm-procs.tcl - no need to worry about them unless you're an OpenACS core hacker.

  2. Verify the deletion of obsolete OpenACS files. The /tcl directory has evolved quite a bit over the months and years, and a few files have come and gone. The /www/doc/removed-files.txt file contains a list of files which must be deleted from the AOLserver installation, at the risk of causing weird conflicts, e.g., having several security filters registered. bootstrap.tcl scans through this list, logging error messages to the log if any of these files exist.

  3. Source *-procs.tcl files in the OpenACS core. We source each file matching the *-procs.tcl glob in the /packages/acs-kernel directory, in lexicographical order. These procedure are needed to perform any of the following steps.

  4. Ensure that the database is available by grabbing and releasing a handle. If we can't obtain a handle, we terminate initialization (since OpenACS couldn't possibly start up the server without access to the database).

  5. Register any new packages in the /packages directory. In each directory inside /packages, we look for a .info file; if we find a package that hasn't yet been registered with the package manager (i.e., it's been copied there manually), we insert information about it into the database. (The first time OpenACS starts up, no packages will have been registered in the database yet, so this step will registers every single package in the /packages directory.) Note that packages discovered here are initially disabled; they must be manually enabled in the package manager before they can be used.

  6. Ensure that the acs-kernel package is enabled. If the OpenACS core isn't initialized, the server couldn't possibly be operational, so if there's no enabled version of the OpenACS core we simply mark the latest installed one as enabled.

  7. Load *-procs.tcl files for enabled packages, activating their APIs.

  8. Load *-init.tcl files for enabled packages, giving packages a chance to register filters and procedures, initialize data structures, etc.

  9. Verify that the core has been properly initialized by checking for the existence of an NSV created by the request processor initialization code. If it's not present, the server won't be operational, so we log an error.

At this point, bootstrap.tcl is done executing. AOLserver proceeds to source the remaining files in the /tcl directory (i.e., unpackaged libraries) and begins listening for connections.

Groups, Context, Permissions

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Pete Su

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

The OpenACS 5.2.3rc1 Permissions system allows developers and administrators to set access control policies at the object level, that is, any application or system object represented by a row in the acs_objects table can be access-controlled via a PL/SQL or Tcl interface. The permissions system manages a data model that then allows scripts to check permissions using another API call.

Although object level permissions seems appropriate, no developer or administrator wants to explicitly set access control rights for every user and every object on a site. Therefore, OpenACS has two auxiliary mechanisms for making this easier:

  1. the Groups system allows users to be grouped together in flexible ways.

  2. the object model defines a notion of object context, which allows applications to group objects together into larger security domains.

The rest of this document discusses each of these parts, and how they fit together with the permissions system.

OpenACS 5.2.3rc1 has an abstraction called a party. Parties have a recursive definition. We can illustrate how it works with the following simplified data model. First, we define the parties table, where each party has an email address and a URL for contact information.

create table parties (
    party_id  integer not null references acs_objects(object_id),
    email varchar(100),
    url varchar(100)
)

Now we define two subtypes of party, one for persons, and one for groups:

create table groups (
    group_id  not null references parties(party_id),
    group_name varchar(100) not null
)

create table persons (
    person_id not null references parties(party_id),
    first_names varchar(100) not null,
    last_name varchar(100) not null
)

The users table is also defined in this data model as a subtype of person.

Finally, we define two relations, one for group membership and one for group composition.

The composition relation expresses that every member of group A should also be a member of group B. This relation allows us to define a hierarchy of groups.

The membership relation maps groups to parties. Each member of a group is a party rather than just a user. That is, groups consist of members that are either a person or an entire group. This allows us to say that group A should be a member of another group B.

The groups data model is recursive. Modelling parties as either a person or a group provides a way to model complex hierarchical groupings of persons and groups.

The full details of the groups data model is beyond the scope of this tutorial. See Parties in OpenACS or OpenACS 4 Groups Design for more details.

NOTE: Much more detailed information about the permissions system and how to use it is available in the OpenACS Permissions Tediously Explained document.

The permissions data model is a mapping between privileges, parties and objects. Parties and objects have already been discussed. Now we focus on privileges.

In OpenACS, a privilege describes the right to perform some operation on some object. Privileges are the basic units out of which we build access control policies. For example in the Unix filesystem, access is controlled by granting users some combination of read, write, or execute privileges on files and directories. In OpenACS 5.2.3rc1, the table of privileges is organized hierarchically so that developers can define privileges that aggregate some set of privileges together. For example, if we have read, write, create and delete privileges, it might be convenient to combine them into a new privilege called "admin". Then, when a user is granted "admin" privilege, she is automatically granted all the child privileges that the privilege contains. The OpenACS 5.2.3rc1 kernel data model defines these privileges:

#
begin
 acs_privilege.create_privilege('read');
 acs_privilege.create_privilege('write');
 acs_privilege.create_privilege('create');
 acs_privilege.create_privilege('delete');
 acs_privilege.create_privilege('admin');

 acs_privilege.add_child('admin', 'read');
 acs_privilege.add_child('admin', 'write');
 acs_privilege.add_child('admin', 'create');
 acs_privilege.add_child('admin', 'delete');

 commit;
end;

Note that a user does not gain admin privileges when granted read, write, create and delete privileges, because some operations explicitly require admin privileges. No substitutions.

To give a user permission to perform a particular operation on a particular object you call acs_permission.grant_permission like this:

# sql code
    acs_permission.grant_permission (
      object_id => some_object_id,
      grantee_id => some_party_id,
      privilege => 'some_privilege_name'
      );

Using just these mechanisms is enough for developers and administrators to effectively define access control for every object in a system.

Explicitly defining permissions to every object individually would become very tedious. OpenACS provides a object contexts as a means for controlling permissions of a large group of objects at the same time.

In OpenACS 5.2.3rc1, object context is a scoping mechanism. "Scoping" and "scope" are terms best explained by example: consider some hypothetical rows in the address_book table:

... scope user_id group_id ...
... user 123 ...
... group 456 ...
... public ...

The first row represents an entry in User 123's personal address book, the second row represents an entry in User Group 456's shared address book, and the third row represents an entry in the site's public address book. In this way, the scoping columns identify the security context in which a given object belongs, where each context is either a person or a group of people or the general public (itself a group of people).

Every object lives in a single context. A context is just an another object that represents the security domain to which the object belongs. By convention, if an object A does not have any permissions explicitly attached to it, then the system will look at the context_id column in acs_objects and check the context object there for permissions. Two things control the scope of this search:

  1. the structure of the context hierarchy itself, and

  2. the value of the security_inherit_p flag in each object.

If security_inherit_p flag is set to 't', then the automatic search through the context happens, otherwise it does not. You might set this field to 'f' if you want to override the default permissions in a subtree of some context.

For an example of how to use context hierarchy, consider the forums application. With only row-level permissions it is not obvious how to reasonably initialize the access control list when creating a message. At best, we have to explicitly grant various read and write privileges whenever we create a message, which is tedious. A reasonable thing to do is to create an object representing a forum, and point the context_id field of a new message at the forum. Then, suppose we grant every user in the system read-access to this forum. By default, they will automatically have read-access to the new message we just inserted, since the system automatically checks permissions on the message's context. To allow the creator of the message to change the message after it has been posted we grant the user write-access on the message, and we are done.

This mechanism allows developers and administrators to define a hierarchy that matches the structure they need for access control in their application. The following picture shows a typical context hierarchy for a hypothetical site:

The top two contexts in the diagram are called "magic" numbers, because in some sense, they are created by default by OpenACS for a specific purpose. The object default_context represents the root of the context hierarchy for the entire site. All permission searches walk up the tree to this point and then stop. If you grant permissions on this object, then by default those permissions will hold for every object in the system, regardless of which subsite they happen to live in. The object security_context_root has a slightly different role. If some object has no permissions attached to it, and its value for security_inherit_p is 'f', or context_id is null, this context is used by default.

See the package developer tutorials for examples on how to use permissions code.

OpenACS 5.2.3rc1 defines three separate mechanisms for specifying access control in applications.

  1. The Groups data model allows you to define hierarchical organizations of users and groups of users.

  2. The Permissions data model allows you to define a hierarchy of user rights.

  3. The Context hierarchy allows you to define organize default permissions in a hierarchical fashion.

A PL/SQL or Tcl API is then used to check permissions in application pages.

Backup Strategy

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

The purpose of backup is to enable recovery. Backup and recovery are always risky; here are some steps that minimize the chance recovery is necessary:

  • Store everything on a fault-tolerant disk array (RAID 1 or 5 or better).

  • Use battery backup.

  • Use more reliable hardware, such as SCSI instead of IDE.

These steps improve the chances of successful recovery:

  • Store backups on a third disk on another controller

  • Store backups on a different computer on a different network in a different physical location. (Compared to off-line backup such as tapes and CDRs, on-line backup is faster and more likely to succeed, but requires maintenance of another machine.)

  • Plan and configure for recovery from the beginning.

  • Test your recovery strategy from time to time.

  • Make it easy to maintain and test your recovery strategy, so that you are more likely to do it.

OpenACS installations comprise files and database contents. If you follow the reference install and put all files, including configuration files, in /var/lib/aolserver/$OPENACS_SERVICE_NAME/, and back up the database nightly to a file in /var/lib/aolserver/$OPENACS_SERVICE_NAME/database-backup, then you can apply standard file-based backup strategies to /var/lib/aolserver/$OPENACS_SERVICE_NAME

The OpenACS Database Access API

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Pete Su and Jon Salz. Modified by Roberto Mello.

One of OpenACS's great strengths is that code written for it is very close to the database. It is very easy to interact with the database from anywhere within OpenACS, and we have a coherent API for database access which makes this even easier.

More detailed information about the DB api is available at Database Access API.

The OpenACS database API is meant to save developers from making common mistakes and to provide a more structured syntax for specifying database operations, including transactions. Here's an example of the API.

set count 0
set tcl_var "foo"
set sql {
      	SELECT foo, bar, baz
       FROM some_table, some_other_table
       WHERE some_table.id = some_other_table.id
         and some_table.condition_p = :tcl_var
}

db_transaction {
    db_foreach my_example_query_name $sql {
        lappend rows [list $foo $bar $baz]
        incr count
    }
    foreach row $rows {
        call_some_proc $foo $bar $baz
    }
}

There are several things to note here:

  1. No explicit code for grabbing and releasing handles. Usage of the Database API implicitly deals with all handle management issues.

  2. The db_transaction command makes the scope of a transaction clear; db_transaction takes the code block argument and automatically runs it in the context of a transaction. If you use something like db_foreach though, you need to make sure that there are no calls in the code block which would take a second db handle since the transaction is only valid for one handle (thats why we build up a list of returned values and call a second proc outside the db_foreach loop).

  3. The command db_foreach writes our old while loop for us.

  4. Every SQL query has a name, which is used in conjunction with .XQL files to support multiple databases.

  5. Finally and most importantly, there API implements bind variables, which we will cover next.

Bind variables are placeholders for literal values in an SQL query being sent to the server. In the old way, data was generally passed to directly to the DB backend, via Tcl string interpolation. In the example above, the query would look like:

select foo, bar, baz
from some_table, some_other_table
where some_table.id=some_other_table.id
and some_table.condition_p = '$foo'

There are a few problems with this:

  1. If the value of $foo is a huge string, then we waste a lot of time in the database server doing useless parsing.

  2. Second, if the literal value contains characters like single quotes, we have to be careful to properly escape them, because not quoting them will lead to surprising errors.

  3. Third, no type checking occurs on the literal value. Finally, if the Tcl variable is passed in or between web forms or otherwise subject to external modification, there is nothing keeping malicious users from setting the Tcl variable to some string that changes the query textually. This type of attack, called SQL smuggling, can be very damaging - entire tables can be exposed or have their contents deleted, for example.

    Another very important reason for using bind variables is performance. Oracle can cache previously parsed queries. If there are values in the where clause, that is how the query is cached. It also performs bind variable susbstitution after parsing the SQL statement. This means that SQL statements that use bind variables will always match (assuming all else is the same) while SQL statements that do not use bind variables will not match unless the values in the statement are exactly the same. This will improve the query cache considerably, which can make the server much more efficient.

What the DB API (in conjuntion with the database drivers implemented for aolserver) do is send the SQL statement to the server for parsing, then bind values to the variables and sends those values along seperately as a second step. This seperate binding step is where the term bind variable comes from.

This split has several advantages. First, type checking happens on the literal. If the column we are comparing against holds numbers, and we send a string, we get a nice error. Second, since string literals are no longer in the query, no extra quoting is required. Third, substitution of bind variables cannot change the actual text of the query, only the literal values in the placeholders. The database API makes bind variables easy to use by hooking them smoothly into the Tcl runtime so you simply provide :tclvar and the value of $tclvar is sent to the backend to actually execute the query.

The database API parses the query and pulls out all the bind variable specifications and replaces them with generic placeholders. It then automatically pulls the values of the named Tcl vars out of the runtime environment of the script, and passes them to the database.

Note that while this looks like a simple syntactic change, it really is very different from how interpolated text queries work. You use bind variables to replace what would otherwise be a literal value in a query, and Tcl style string interpolation does not happen. So you cannot do something like:

set table "baz"
set condition "where foo = bar"

db_foreach my_query { select :table from some_table where :condition }
    

SQL will not allow a literal to occur where we've put the bind variables, so the query is syntactically incorrect. You have to remember that while the bind variable syntax looks similar to variable interpolation in Tcl, It is not the same thing at all.

Finally, the DB API has several different styles for passing bind variable values to queries. In general, use the style presented here because it is the most convenient.

Every db_* command accepting a SQL command as an argument supports bind variables. You can either

  • Specify the -bind switch to provide a set with bind variable values, or

  • Specify the -bind switch to explicitly provide a list of bind variable names and values, or

  • Not specify a bind variable list at all, in which case Tcl variables are used as bind variables.

The default behavior (i.e., if the -bind switch is omitted) is that these procedures expect to find local variables that correspond in name to the referenced bind variables, e.g.:

set user_id 123456
set role "administrator"

db_foreach user_group_memberships_by_role {
    select g.group_id, g.group_name
    from user_groups g, user_group_map map
    where g.group_id = map.user_id
    and map.user_id = :user_id
    and map.role = :role
} {
    # do something for each group of which user 123456 is in the role
    # of "administrator"
}

      

The value of the local Tcl variable user_id (123456) is bound to the user_id bind variable.

The -bind switch can takes the name of an ns_set containing keys for each bind variable named in the query, e.g.:

set bind_vars [ns_set create]
ns_set put $bind_vars user_id 123456
ns_set put $bind_vars role "administrator"

db_foreach user_group_memberships_by_role {
    select g.group_id, g.group_name
    from user_groups g, user_group_map map
    where g.group_id = map.user_id
    and map.user_id = :user_id
    and map.role = :role
} -bind $bind_vars {
    # do something for each group in which user 123456 has the role
    # of "administrator"
}

      

Alternatively, as an argument to -bind you can specify a list of alternating name/value pairs for bind variables:

	
db_foreach user_group_memberships_by_role {
    select g.group_id, g.group_name
    from user_groups g, user_group_map map
    where g.group_id = map.user_id
    and map.user_id = :user_id
    and map.role = :role
} -bind [list user_id 123456 role "administrator"] {
    # do something for each group in which user 123456 has the role
    # of "administrator"
}

      

When processing a DML statement, Oracle coerces empty strings into null. (This coercion does not occur in the WHERE clause of a query, i.e. col = '' and col is null are not equivalent.)

As a result, when using bind variables, the only way to make Oracle set a column value to null is to set the corresponding bind variable to the empty string, since a bind variable whose value is the string "null" will be interpreted as the literal string "null".

These Oracle quirks complicate the process of writing clear and abstract DML difficult. Here is an example that illustrates why:

#
# Given the table:
#
#   create table foo (
#           bar        integer,
#           baz        varchar(10)
#   );
#

set bar ""
set baz ""

db_dml foo_create "insert into foo(bar, baz) values(:bar, :baz)"
#
# the values of the "bar" and "baz" columns in the new row are both
# null, because Oracle has coerced the empty string (even for the
# numeric column "bar") into null in both cases

      

Since databases other than Oracle do not coerce empty strings into null, this code has different semantics depending on the underlying database (i.e., the row that gets inserted may not have null as its column values), which defeats the purpose of SQL abstraction.

Therefore, the Database Access API provides a database-independent way to represent null (instead of the Oracle-specific idiom of the empty string): db_null.

Use it instead of the empty string whenever you want to set a column value explicitly to null, e.g.:

set bar [db_null]
set baz [db_null]

db_dml foo_create "insert into foo(bar, baz) values(:bar, :baz)"
#
# sets the values for both the "bar" and "baz" columns to null

The database library can transparently maintain pools of sequence values, so that each request for a new sequence value (using db_nextval) does not incur a roundtrip to the server. For instance, this functionality is very useful in the security/sessions library, which very frequently allocates values from the sec_id_seq sequence. To utilize this functionality for a particular sequence, register the sequence to be pooled, either using the db_register_pooled_sequence procedure at server startup time, or by including a configuration parameter of the form

PoolSequence.sequence_name_seq=count

in any configuration section in the yourservername.ini file, e.g.,

[ns/server/yourservername/acs/security]
PoolSequence.sec_id_seq=20

    

The database library will allocate this number of sequence values at server startup. It will periodically scan pools and allocate new values for sequences which are less than half-full. (This normally occurs every 60 seconds, and is configurable via the PooledSequenceUpdateInterval parameter in the [ns/server/yourservername/acs/database] configuration section.)

The Database API has several functions that wrap familiar parts of the AOLserver database API.

Note that you never have to use ns_db anymore (including ns_db gethandle)! Just start doing stuff, and (if you want) call db_release_unused_handles when you're done as a hint to release the database handle.

db_abort_transaction
db_abort_transaction
	  

Aborts all levels of a transaction. That is if this is called within several nested transactions, all of them are terminated. Use this insetead of db_dml "abort" "abort transaction".

db_multirow
db_multirow [ -local ] [ -append ] [ -extend column_list ]  var-namestatement-namesql  [ -bind bind_set_id | -bind bind_value_list ]  code_block [ if_no_rows if_no_rows_block ]

Performs the SQL query sql, saving results in variables of the form var_name:1, var_name:2, etc, setting var_name:rowcount to the total number of rows, and setting var_name:columns to a list of column names.

Each row also has a column, rownum, automatically added and set to the row number, starting with 1. Note that this will override any column in the SQL statement named 'rownum', also if you're using the Oracle rownum pseudo-column.

If the -local is passed, the variables defined by db_multirow will be set locally (useful if you're compiling dynamic templates in a function or similar situations).

You may supply a code block, which will be executed for each row in the loop. This is very useful if you need to make computations that are better done in Tcl than in SQL, for example using ns_urlencode or ad_quotehtml, etc. When the Tcl code is executed, all the columns from the SQL query will be set as local variables in that code. Any changes made to these local variables will be copied back into the multirow.

You may also add additional, computed columns to the multirow, using the -extend { col_1col_2 ... } switch. This is useful for things like constructing a URL for the object retrieved by the query.

If you're constructing your multirow through multiple queries with the same set of columns, but with different rows, you can use the -append switch. This causes the rows returned by this query to be appended to the rows already in the multirow, instead of starting a clean multirow, as is the normal behavior. The columns must match the columns in the original multirow, or an error will be thrown.

Your code block may call continue in order to skip a row and not include it in the multirow. Or you can call break to skip this row and quit looping.

Notice the nonstandard numbering (everything else in Tcl starts at 0); the reason is that the graphics designer, a non programmer, may wish to work with row numbers.

Example:

db_multirow -extend { user_url } users users_query {
    select user_id first_names, last_name, email from cc_users
} {
    set user_url [acs_community_member_url -user_id $user_id]
}
    

You can also iterate over a multirow after it has been created - check the documentation for template::multirow

For example,

db_multirow assets assets {
  select asset_id,
    from ...
}

..

set asset_id_l [list]
multirow foreach assets {
  lappend asset_id_l $asset_id
}
          

Technically it's equivalent to using a code block on the end of your db_multirow.

db_null
db_null

Returns a value which can be used in a bind variable to represent the SQL value null. See Nulls and Bind Variables above.

db_foreach
db_foreach statement-name sql [ -bind bind_set_id | -bind bind_value_list ]  [ -column_array array_name | -column_set set_name ]  code_block [ if_no_rows if_no_rows_block ]

Performs the SQL query sql, executing code_block once for each row with variables set to column values (or a set or array populated if -column_array or column_set is specified). If the query returns no rows, executes if_no_rows_block (if provided).

Example:

db_foreach select_foo "select foo, bar from greeble" {
    doc_body_append "<li>foo=$foo; bar=$bar\n"
} if_no_rows {
    doc_body_append "<li>There are no greebles in the database.\n"
}

	  

The code block may contain break statements (which terminate the loop and flush the database handle) and continue statements (which continue to the next row of the loop).

db_1row
db_1row statement-namesql [ -bind bind_set_id | -bind bind_value_list ]  [ -column_array array_name | -column_set set_name ]
	  

Performs the SQL query sql, setting variables to column values. Raises an error if the query does not return exactly 1 row.

Example:

db_1row select_foo "select foo, bar from greeble where greeble_id = $greeble_id"
# Bombs if there's no such greeble!
# Now $foo and $bar are set.

	  
db_0or1row
db_0or1row statement-namesql [ -bind bind_set_id | -bind bind_value_list ]  [ -column_array array_name | -column_set set_name ]
	  

Performs the SQL query sql. If a row is returned, sets variables to column values and returns 1. If no rows are returned, returns 0. If more than one row is returned, throws an error.

db_nextval
db_nextval sequence-name

Returns the next value for the sequence sequence-name (using a SQL statement like SELECTsequence-name.nextval FROM DUAL). If sequence pooling is enabled for the sequence, transparently uses a value from the pool if available to save a round-trip to the database (see Sequence Pooling).

db_register_pooled_sequence
db_register_pooled_sequence sequence-namepool-size

Registers the sequence sequence-name to be pooled, with a pool size of pool-size sequence values (see Sequence Pooling).

db_string
db_string statement-namesql [ -default default ] [ -bind bind_set_id | -bind bind_value_list ]
	  

Returns the first column of the result of SQL query sql. If sql doesn't return a row, returns default (or throws an error if default is unspecified). Analogous to database_to_tcl_string and database_to_tcl_string_or_null.

db_list
db_list statement-namesql [ -bind bind_set_id | -bind bind_value_list ]
	  

Returns a Tcl list of the values in the first column of the result of SQL query sql. If sql doesn't return any rows, returns an empty list. Analogous to database_to_tcl_list.

db_list_of_lists
db_list_of_lists statement-namesql [ -bind bind_set_id | -bind bind_value_list ]
	  

Returns a Tcl list, each element of which is a list of all column values in a row of the result of SQL query sql. If sql doesn't return any rows, returns an empty list. (Analogous to database_to_tcl_list_list.)

db_dml
db_dml statement-namesql  [ -bind bind_set_id | -bind bind_value_list ]  [ -blobs blob_list | -clobs clob_list |
      -blob_files blob_file_list | -clob_files clob_file_list ]
	  

Performs the DML or DDL statement sql.

If a length-n list of blobs or clobs is provided, then the SQL should return n blobs or clobs into the bind variables :1, :2, ... :n. blobs or clobs, if specified, should be a list of individual BLOBs or CLOBs to insert; blob_files or clob_files, if specified, should be a list of paths to files containing the data to insert. Only one of -blobs, -clobs, -blob_files, and -clob_files may be provided.

Example:

db_dml insert_photos "
        insert photos(photo_id, image, thumbnail_image)
        values(photo_id_seq.nextval, empty_blob(), empty_blob())
        returning image, thumbnail_image into :1, :2
    "  -blob_files [list "/var/tmp/the_photo" "/var/tmp/the_thumbnail"]

	  

This inserts a new row into the photos table, with the contents of the files /var/tmp/the_photo and /var/tmp/the_thumbnail in the image and thumbnail columns, respectively.

db_write_clob, db_write_blob, db_blob_get_file
db_write_clob statement-namesql [ -bind bind_set_id | -bind bind_value_list ]

db_write_blob statement-namesql [ -bind bind_set_id | -bind bind_value_list ]

db_blob_get_file statement-namesql [ -bind bind_set_id | -bind bind_value_list ]
	  

Analagous to ns_ora write_clob/write_blob/blob_get_file.

db_release_unused_handles
	    db_release_unused_handles
	  

Releases any allocated, unused database handles.

db_transaction
db_transaction code_block [ on_error { code_block } ]
	  

Executes code_block transactionally. Nested transactions are supported (end transaction is transparently ns_db dml'ed when the outermost transaction completes). The db_abort_transaction command can be used to abort all levels of transactions. It is possible to specify an optional on_error code block that will be executed if some code in code_block throws an exception. The variable errmsg will be bound in that scope. If there is no on_error code, any errors will be propagated.

Example:

proc replace_the_foo { col } {
    db_transaction {
        db_dml "delete from foo"
        db_dml "insert into foo(col) values($col)"
    }
}

proc print_the_foo {} {
    doc_body_append "foo is [db_string "select col from foo"]<br>\n"
}

replace_the_foo 8
print_the_foo ; # Writes out "foo is 8"

db_transaction {
    replace_the_foo 14
    print_the_foo ; # Writes out "foo is 14"
    db_dml "insert into some_other_table(col) values(999)"
    ...
    db_abort_transaction
} on_error {
    doc_body_append "Error in transaction: $errmsg"
}


print_the_foo ; # Writes out "foo is 8"

	  
db_resultrows
db_resultrows
	  

Returns the number of rows affected or returned by the previous statement.

db_with_handle
db_with_handle varcode_block

Places a database handle into the variable var and executes code_block. This is useful when you don't want to have to use the new API (db_foreach, db_1row, etc.), but need to use database handles explicitly.

Example:

proc lookup_the_foo { foo } {
    db_with_handle db {
        return [db_string unused "select ..."]
    }
}

db_with_handle db {
    # Now there's a database handle in $db.
    set selection [ns_db select $db "select foo from bar"]
    while { [ns_db getrow $db $selection] } {
        set_variables_after_query

        lookup_the_foo $foo
    }
}

	  
db_nullify_empty_string
db_nullify_empty_string string

For true SQL purists, we provide the convenience function db_nullify_empty_string, which returns [db_null] if its string argument is the empty string and can be used to encapsulate another Oracle quirk:

set baz ""

# Clean out the foo table
#
db_dml unused "delete from foo"

db_dml unused "insert into foo(baz) values('$baz')"

set n_rows [db_string unused "select count(*) from foo where baz is null"]
#
# $n_rows is 1; in effect, the "baz is null" criterion is matching
# the empty string we just inserted (because of Oracle's coercion
# quirk)

	  

To balance out this asymmetry, you can explicitly set baz to null by writing:

db_dml foo_insert "insert into foo(baz) values(:1)" {[db_nullify_empty_string $baz]}

	  

OpenACS Edit This Page Templates

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

by Nick Carroll

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.
  • Learn about the OpenACS templating system.

  • Learn about subsites and site-map administration.

The OpenACS templating system allows you to give your site a consistent look and feel. It also promotes code maintainability in the presentation layer, by allowing presentation components to be reused across multiple pages. If you need to change the layout for some reason, then you only need to make that change in one location, instead of across many files.

In this problem set you will familiarise yourself with the templating system in openacs. This will be achieved through customising an existing edit-this-page application template.

Before proceeding, it is strongly advised to read the templating documentation on your openacs installation (http://localhost:8000/doc/acs-templating). The documentation lists the special tags available for ADP files.

  • Create a subsite called pset3.

  • A subsite is simply a directory or subdirectory mounted at the end of your domain name. This can be done in one of two places:

    • http://localhost:8000/admin/site-map

    • or the subsite admin form on the main site, which is available when you login to your OpenACS installation.

  • Checkout ETP from CVS:

    cd ~/openacs/packages
                cvs -d:pserver:anonymous@openacs.org:/cvsroot login
                cvs -d:pserver:anonymous@openacs.org:/cvsroot co edit-this-page
  • Go to the package manager at http://yoursite/acs-admin/apm. And install the new package: edit-this-page.

  • Or use the "Add Application" form available on the Main site.

  • Work out how to change the ETP application.

  • Investigate each of the available ETP templates:

    • Default

    • News

    • FAQ

  • Browse the files for each of the above ETP templates at:

    cd ~/openacs/packages/edit-this-page/templates
  • Use the article template as the basis of our new col2 template.

    cp article-content.adp col2-content.adp
                cp article-content.tcl col2-content.tcl
                cp article-index.adp col2-index.adp
                cp article-index.tcl col2-index.tcl
  • The template should provide us with the following ETP layout:

    Table10.1.table showing ETP layout

    Header
    Sidebar Main Content Pane

  • The "Main Content" pane should contain the editable content that ETP provides.

  • The "Header" should display the title of the page that you set in ETP.

  • The "Sidebar" should display the extlinks that you add as a content item in ETP.

  • Need to register your template with ETP so that it appears in the drop-down menu that you would have seen in Exercise 3.

    cd ~/openacs/packages/edit-this-page/tcl
                emacs etp-custom-init.tcl
  • Use the function etp::define_application to register your template with ETP

    • Uncomment the "asc" definition

    • Set allow_extlinks to true, the rest should be false.

  • Restart your server for the changes to take effect.

  • Configure your ETP instance at /lab4/index to use the col2 template.

  • Create external links to link to other mounted ETP instances.

  • Check that your external links show up in the sidebar when you view your ETP application using the col2 template.

This problem set was originally written by Nick Carroll in August 2004 for the University of Sydney Course EBUS5002.

This material is copyright 2004 by Nick Carroll. It may be copied, reused, and modified, provided credit is given to the original author.

Next Page