Forum OpenACS Development: Howto: Expand and Use acs-subsite Based Member Roles

As a result of asking many questions, studying code, and tinkering, I have documented the process I used to create additional acs-subsite roles (in addition to Members and Administrators). This document describes the use of relation types, relational segments, and application groups, within the context of an acs-subsite. It adds two roles: Guests and Managers, and contains sample tcl api and db calls used to implement the expansion. It should be general enough to be used as guide for others. I am interested in scrutiny by "those that know", but my real purpose is to provide a shortcut for others that may wish to do the same. Randy

OACS Subsite Community Roles

Author:    Randy O'Meara <omeara at got dot net>
Date:        17 August 2003

Summary

This document describes the addition and utilization of an expanded role system. This expanded role system is based on acs-subsite, its associated application group, and relational segments. We assume that the application group is created (as it is in oacs 4.6) at subsite instantiation. At the same time, two relational segments are also created: the Members and Administrators segments. This document describes the steps and calls required to create and assign additional roles within an subsite-based community.

The reason I created this extended role system was to provide more granularity in assigning rights and permissions to community members. I wanted to allow access and services to four levels of users, where each increasing level included the rights of the next lower level *plus* some additional rights. The levels I defined, from lowest to highest, were: Guest, Member, Manager, and Administrator. In order to enforce and control access to each separate community, it is necessary to change some default settings including turning of inheritance of permissions from the main site. These steps are included below but are optional if strict community member control and privacy are not at issue.

This document describes one way to go about creating the system. There are decisions to be made at every step, the results of which may lead to other ways to achieve an equivalent end result. For example, it is not necessary to create the pretty-named roles. The decision to not do this  would result in the role names not being available through a DB query later to generate a drop-down selection list. However, the role names and their values could be hard coded at the appropriate place and used in place of the query that retrieves this information.

Beware... This document was created after I spent hours researching, perusing code, posting to forums, tinkering, and testing. Thanks to the few that guided me in the right direction! Part of the difficulty in creating this code was that there really are (at least, were) no working examples of a role system based on acs-subsite and application groups. As such, there may be far easier methods to do what I've done here. I have (minimally) tested the code included in the examples below. But...not exhaustively, and NOT IN PRODUCTION. I am interested in errors you may find. Please let me know what you find.

The process described below consists of the following steps:
1) Create Relation Types
2) Instantiate Subsite
3) Modify Default Subsite Policy
4) Create Relational Segments and Apply Privileges
5) Assign User to a Role
6) Use the OACS permissions:: System to Control Actions and Access

Relation Types Creation

Recall that we need only create the Guest and Manager roles since the Member and Administrator roles already exist.

The first step is to create pretty-named roles:
set role guest; set pn Guest; set pp Guests
db_1row new_role {
        select acs_rel_type__create_role(:role, :pn, :pp)
}

set role manager; set pn Manager; set pp Managers
db_1row new_role {
        select acs_rel_type__create_role(:role, :pn, :pp)
}
The second step is to create relation types:

            # Create rel_types
            set m_rel_type [rel_types::new \
                    -supertype "membership_rel" \
                    -role_one "" \
                    -role_two "guest" \
                    "guest_rel" \
                    "Guest" \
                    "Guests" \
                    "group" \
                    "0" \
                    "" \
                    "person" \
                    "0" \
                    "1"]
            rel_types::add_permissible application_group guest_rel

            set m_rel_type [rel_types::new \
                    -supertype "membership_rel" \
                    -role_one "" \
                    -role_two "manager" \
                    "manager_rel" \
                    "Manager" \
                    "Managers" \
                    "group" \
                    "0" \
                    "" \
                    "person" \
                    "0" \
                    "1"]
            rel_types::add_permissible application_group manager_rel

Subsite Instantiation

A subsite can be instantiated and mounted directly below the root (Main Site) with the following tcl api call:
set comm_obj_id [site_node::instantiate_and_mount \
                 -node_name "subsite1" \
                 -package_name "subsite1 Community" \
                 -package_key "acs-subsite"]
The subsite is created, mounted in the site map, and accessible at "/subsite1". The community package_id (comm_obj_id) is returned and used to further modify the subsite. All of the information for this subsite node (including its package_id) may be retrieved at a later time with site_node::get_from_url -url "/subsite1" .

Subsite Policy Modification

Subsite permission inheritance can be eliminated with the permissions subsystem.
# Do not inherit permissions from the main site
permission::set_not_inherit -object_id $comm_obj_id
The subsite member join policy can be restricted.
# Get the subsite application group
set app_grp [application_group::group_id_from_package_id \
                -package_id $comm_obj_id]

# Set subsite application group join_policy to "needs approval"
db_dml update_join_policy {
        update groups
        set join_policy = 'needs approval'
        where group_id = :app_grp
}

# There is another location to set the join policy?
# Oh well, for good measure...
parameter::set_value -package_id $comm_obj_id \
        -parameter RegistrationRequiresApprovalP -value 1

Relational Segments Create & Permission

When all is said and done, we want four relational segments that coincide with the four roles we defined earlier. Two of these segments already exist by way of acs-subsite instantiation: Members and Administrators. We need to create the Guests and Managers segments. In addition to creating the segments, we need to assign the standard oacs privileges to each segment. The code below actually removes the "create" privilege from the previously created Members segment. You may not want to do this. Later, a user will be related to the subsite application group through a particular type of relation, and the user will obtain privileges based on the relation.
# Create and grant: Guests
set g_seg [rel_segments_new $app_grp \
        guest_rel "subsite1 Guests"]
permission::grant -party_id $g_seg \
        -object_id $comm_obj_id -privilege read

# Create and grant: Managers
set m_seg [rel_segments_new $app_grp \
        manager_rel "subsite1 Managers"]
permission::grant -party_id $m_seg \
        -object_id $comm_obj_id -privilege read
permission::grant -party_id $m_seg \
        -object_id $comm_obj_id -privilege create
permission::grant -party_id $m_seg \
        -object_id $comm_obj_id -privilege write

# Get the "Members" segment and remove the create priv
set u_seg [db_string rel_seg_id_get {
        select segment_id
        from rel_segments
        where group_id = :app_grp
        and rel_type= 'membership_rel'
} -default ""]
permission::revoke -party_id $u_seg \
        -object_id $comm_obj_id -privilege create

# Get the "Administrators" segment
set a_seg [db_string rel_seg_id_get {
        select segment_id
        from rel_segments
        where group_id = :app_grp
        and rel_type= 'admin_rel'
} -default ""]
The last section "Get the Administrators segment" is shown for completeness although it's not necessary. This also shows how to retrieve the segment IDs if needed. You might want to stash g_seg, u_seg, m_seg, and a_seg.

The subsite is created, mounted in the site map, and accessible at "/subsite1". The community package_id (comm_obj_id) is returned and used to further modify the subsite. All of the information for this subsite node (including its package_id) may be retrieved at a later time with the site_node::get_from_url -url "/subsite1" tcl api call.

Community Member Role Assignment

Ahhh! We can finally assign a prospective community member to a role. I'm certain that there are many methods of obtaining the user_id that we wish to use in role assignment. The method I used was to have the cummunity applicant apply for membership through a slightly modified /register/user-join page. The standard oacs user-join page was modified to accept (in its query vars) a valid application group_id, an invitation code, and then to validate the user by a call to ad_maybe_redirect_for_registration. This modified page, along with the subsite "join policy" changes that were made above, result in:

    1) a gurantee that the applicant is a Main Site "Registered User", and
    2) the applicant possesses the correct invitation code.

Once these conditions are staisfied, the user-join page adds the applicant to the subsite's application group, but with a member_state of "needs approval". Until a subsite Manager or Administrator assigns the applicant to one of the pre-defined roles, the applicant cannot access the subsite. The action of assigning the applicant to a role simultaneously changes the applicant's member_state to "approved".

The process of assigning the applicant to a community role can be performed any number of ways. One way might be to use ad_form with a drop-down role list populated by
# Get subsite node_id
set role_opts [group::get_rel_types_options \
        -group_id $app_grp]
lappend role_opts [list -REMOVE- -REMOVE-]
and use the "-REMOVE-" flag to trigger a relation removal.

Ultimately, to change/add/remove a user in a role, the following tcl api calls are used. To change a user's role, first remove the existing relation and then add the new relation:
# Retrieve a user's role along with other user information
set rel_id [db_0or1row role_get {
      select
          pe.last_name || ', ' || pe.first_names as name,
          pa.email as email,
          pe.person_id as member_id,
          mm.rel_type as role,
          mm.rel_id as rel_id,
          mr.member_state as member_state
      from group_member_map mm,  membership_rels mr,  
          parties pa, persons pe
      where mm.member_id = pe.person_id
      and mm.member_id = pa.party_id
      and mm.rel_id = mr.rel_id
      and mm.group_id = :app_grp
      and pe.person_id = :member_id
}]

# Remove a user from a role
relation_remove $rel_id

# Add a user in a role
relation_add -member_state "approved" \
        $role $app_grp $member_id

Use OACS Permissions System

You should now be set to use the standard AOCS permissions:: system to determine what actions your pages should present to the user based on the privileges assigned to the user through his/her association with the assigned role.

Collapse
Posted by Robert Locke on
Great stuff Randy!  Thanks for posting this.  Hopefully, this will find its way into our documentation or some place more obvious than this thread.

Quick questions: Where can we access your modified user-join page? Is it possible to accomplish any or all of the above through the admin UI rather than the Tcl API?

Thanks...

Collapse
Posted by Jun Yamog on
Hi Randy,

Great doc, very useful to me.  I hope it makes to the docs.  It would be sad if it gets buried in the forums.

Collapse
Posted by Randy O'Meara on
Robert: glad to help. The first pass at user-join fixes is logged as a bug with patch against 4.6 CVS tip at https://openacs.org/bugtracker/openacs/com/acs-subsite/patch?patch_number=259. I made some additional changes, including the invitation_code requirement, to a local copy that I now use in my development.  You can grab the patch if you want. If you want the other changes, let me know and I'll email my local copy to you.

Jun: again, glad to help.

Randy

P.S. I see a couple typos in the above post, but nothing that will affect the sample code. There is a repeated paragraph at the end of the "Relational Segments Create & Permission" section that should be deleted. It was a leftover from a copy/past of a previous section. The last section says "AOCS" and it should be "OACS"...fumble fingers.

Randy,

Thank you very much for the research and the write-up. It sure saved me a tremendous amount of effort (working on my first OACS package).

I would like to add that there is a bit of a problem when uninstalling a package and the relationship type must be removed. There is no TCL API opposite to rel_types::new and the SQL API acs_rel_type__drop_type function does not remove the table that is created by rel_types::new.

After some tinkering, I came up with the following. It should uninstall the relationship type cleanly if nothing else is triggered by rel_types::new. I have only had time to test this briefly by repeatedly installing/uninstalling an otherwise unused package. So far, it seems to work OK.

set ext "_ext"
rel_types::remove_permissible application_group $m_rel_type
set rel_type_ext $m_rel_type$ext
db_exec_plsql drop_table "drop table $rel_type_ext"
db_exec_plsql drop_rel_type "select acs_rel_type__drop_type(:m_rel_type, false);"

This is not very clean, but my first attempt to embed everything in pgplsql code in a single query failed. I couldn't figure out how to pass a variable to "drop table" and I didn't want to hardcode the type name in the query.

You are very welcome. And thanks for adding your notes on cleanup.

In general, once a package ties itself into oacs by adding system objects, types, and permissions, removing the package is like extracting teeth. It requires brute-force.

I hope someone corrects me on the previous statement...

Brute force? No no, it requires a fileting knife, and much persistance and testing. It's not that bad if you write your drop scripts as you develop the package - that's when you may actually want to drop and recreate it over and over again, wiping out all data every time you drop it. If you leave the drop scripts till later, well, I dunno. Get to work with the filet knife, I guess. (I've never had rel types I needed to drop though.)
Collapse
Posted by Randy O'Meara on
As I sharpened my filet knife I realized that the roles and types I had created were general-purpose roles that I may want to use in other packages and such. So instead of carving the roles from the system, I refactored the code that creates roles, types, and associates thier use with Application Groups so that it would extend whatever roles were present with the roles that were not present. Here's the new code. BTW, this makes it a very simple matter to extend the roles further by adding a single entry in the roles_rel list structure. Maybe this is a canditate to add to the acs-subsite admin UI?

ad_proc -private rel_types_extend {
} {

    Extend the standard oacs application group roles and relation types of
    Members and Administrators with Guests and Managers.

    Since these additional roles/types are general purpose, we'll leave
    them installed when our package is removed. We'll also use the roles
    if they already exist, whether we created them or something else did.

    When this proc completes, the following will exist:

    	--Role--      --Rel--
    	Guest	      guest_rel (extension)
    	Member	      membership_rel (standard)
    	Manager	      manager_rel (extension)
    	Administrator admin_rel (standard)

    There is the assumption that, if a required role already exists,
    then it is correctly configured with the proper rel_type and that 
    it is also a registered type for Application Groups. Conversely,
    if the role does not yet exist, then the role, its rel_type, and
    association with group type Application Group is configured.
} {

    # Map required roles to rels (add new roles & rels here)
    #
    #                     --Role--      --Pretty--    --Plural--     --Rel Type--
    set roles_rels "[list guest	        Guest         Guests         guest_rel] \
		    [list member        Member        Members        membership_rel] \
		    [list manager       Manager       Managers       manager_rel] \
		    [list admin         Administrator Administrators admin_rel]"

    foreach {role pn pp rel} $roles_rels {

	# Base existence check on existing role
	if {[db_0or1row role_exists {
	    select r.pretty_name from acs_rel_roles r where r.role=:role
	}]} {

	} else {

	    # Extend roles
	    db_1row new_role {select acs_rel_type__create_role(:role, :pn, :pp)}

	    # Extend rel types
	    set m_rel_type [rel_types::new -supertype "membership_rel" \
			    -role_one "" -role_two $role \
			    $rel $pn $pp "group" "0" "" "person" "0" "1"]

	    # Associate with group type Application Group
	    rel_types::add_permissible application_group $rel
	}
    }
}
Collapse
Posted by xx xx on
I would like to know whether the method described above is the preferred way of creating (sub)groups in openACS 5.0. I mean: use application groups to associate groups with a package (subsite) and relsegs to create (sub)groups.
Does the core team agree on this?
I am interested in any and all alternative methods that  achieve the same result.
Collapse
Posted by Juanjo Ruiz on
Hi all,

It seems like this is the best thread explaining how to model user/group/roles in openACS for a posterior use of the permission system.

In my model there is a slightly difference which results in the impossibility to apply this process.
My new roles are not ‘membership_rel’ based but ‘composite_rel’ based.
I mean I want a ‘company_member’  new rel_type with ‘composite_rel’ as its supertype.

I’ve followed your process and all seemed to work fine, but at the end, when I try to add a group/company (not a user) in the ‘company_member’ role (set rel_id [relation_add "cluster_company" $app_grp $company_id]) what I do is to link that party to the application_group instead of the rel_segment.
But like the granted permission that I’ve added is for the rel_segment (permission::grant –party_id comp_seg  object_id $comp_obj_id –privilege write) if I add a member to the group/company (membership_rel), neither that member nor the group/company, have ‘write’ privilege on the subsite.

What I’ve thought is, ok then what I need is to add the group with a ‘composite_rel’ to the rel_segment instead of creating a new ‘company_member’ relation between the application_group and the group/company.
But well, the ‘composition_rel’ only accepts a ‘group’ object in its first argument (object_id_one) and rel_segment is a subtype of party.

I don’t know if I have explained very well my problem, but any light that you can throw over me it would be wonderful.

Collapse
Posted by Dave Bauer on
I am not sure from your example code. THe -object_id parameter of you grant_permission call needs to refer to the subsite_id of the subsite you want to grant permssion on.
Collapse
Posted by Talli Somekh on
BTW, did Randy's doc make it into the OACS docs? Or perhaps on Jade's list of important OACS links?

talli

Collapse
Posted by Juanjo Ruiz on
Yes, sorry, bad copy-paste, It would have been:

(permission::grant -party_id $comp_seg \
                -object_id $package_id -privilege read)

I am going to be more especific.

I have a Project subsite, only members of the companies which belongs to the 'project_company' (and more) segments can access the site. It is no more than a acs-subsite with edit-this-page. An I want to create three different roles, not by user(members) but by groups(companies).

First I create the three rel_types like this one:

  acs_rel_type.create_type (
    rel_type => 'project_manager',
    pretty_name => 'Project Manager',
    pretty_plural => 'Project Managers',
    supertype => 'composition_rel',
    object_type_one => 'application_group',
    role_one => 'project',
    table_name => 'PROJECT',
    id_column => 'project_id',
    package_name => 'project_managers',
    min_n_rels_one => 1, max_n_rels_one => null,
    object_type_two => 'group',
    min_n_rels_two => 1, max_n_rels_two => 1
  );

Then I create this rel_segments:

        # Create and grant: Project Companies
        set comp_seg [rel_segments_new $app_grp \
                project_company "$instance_name Companies"]
        permission::grant -party_id $comp_seg \
                -object_id $package_id -privilege read

        # Create and grant: Project Manager
        set m_seg [rel_segments_new $app_grp \
                project_manager "$instance_name Manager"]
        permission::grant -party_id $m_seg \
                -object_id $package_id -privilege admin

        # Create and grant: Project Client
        set cli_seg [rel_segments_new $app_grp \
                project_client "$instance_name Clients"]
        permission::grant -party_id $cli_seg \
                -object_id $package_id -privilege admin

What I guest I've done until here is to create three Group/Company roles and assigned permissions to each role.

I only need to add Groups/Companies to each Role. Then I create a new object (in this case is a new 'company' which is a subtype of 'group') and following the proccess explained in this topic I do:

set rel_id [relation_add -member_state "approved" project_company $app_grp $company_id]

Then I can add contacts to this company:

set contact_id [ad_user_new $email $name $surname $password "" "" "" "t" "approved"]
set rel_id [relation_add -member_state "approved" membership_rel $company_id $contact_id]

So far so good. But the new user cannot access to the project subsite. And if I see the code that's quite understandable. I've added the company to the subsite's application group, not to the comp_seg rel_segment. Maybe the '-member_state approved' makes some magic glue... but when I look at 'package_instantiate_object'  I get lost.

If I see the permissions of the user_id (select * from acs_permissions where grantee_id = 26226), only has read and write for itself.
If I see the group_member_index of the user_id, he belongs to -2,-1,company_id,application_group_id.

If I see the permissions on the package_id there are read, admin, admin for the three rel_segments.

That's why I wanted to do a composition_rel between the company and the rel_segment. But the system do not allow to me to do that. Must I create one rel_segment per company?

Collapse
Posted by Dave Bauer on
You do need the composition_rels between the groups. But I think you might be going about it a litle wrong with the relational segments.

You still want to add users in a role. I don't think relational segments work between composition_rels, only membership_rels. So you would create users as a certain type of member in each company group.

Now you can access the relational segment of that company group with a certain role/relationship_type.

Collapse
Posted by Juanjo Ruiz on
Ok, thanks Dave. With membership_rel works pretty well.

Talli: My vote is yes, sometimes is better a good example than an awfull lot of documentation.

Collapse
Posted by Stan Kaufman on
A couple comments about Randy's excellent how-to about subsites and roles in this thread from nearly two years ago:

  • There's now a tcl api for creating acs_rel_roles: rel_types::create_role (Malte added this earlier this year). So the plsql in Step 1 shouldn't be used, since Malte's proc automagically internationalizes the role for you.

  • There is a surprising gotcha in rel_types::new due to its use of a couple utility procs that munge out names for the behind-the-scenes helper tables and their constraints (plsql_utility::generate_oracle_name and plsql_utility::generate_constraint_name). The problem is that these procs take the strings for the parameter rel_type or the switch table_name and create "oracle-safe" names from the "initials" from these strings -- as parsed from underscores. What can happen is that two very different rel_types can produce the same primary or foreign key constraint -- causing the rel_types::new proc to barf.

    For example, if you have these two roles: foofoofoofoo_admin and fumfumfumfum_admin, these procs will generate the same primary key constraint: FAE_REL_ID_PK from both rel_types. This fails the second time, obviously, since two such constraints cannot have the same name.

    Worse, rel_types::new doesn't rollback cleanly for reasons that escape me.

    Anyway, the optimal solution would be for these utility procs to generate unique results (which they admit they don't). Another workaround would be for rel_types::new to append the next object_id from acs_object_id_seq before sending the value to the constraint name procs, but that actually wouldn't work reliably since they split along underscores, so foofoofoofoo_admin_1001 and fumfumfumfum_admin_1002 would still both turn into FA1E_REL_ID_PK.

    The workaround I used for my purposes (bodily migrating all the roles from 3.2.5 sites at once) is to check for uniqueness in the calling code. This obviously isn't a permanent solution, though, as subsequent collisions may still happen when new roles are added at a later date. And at whatever point a UI is built to allow admin users -- and not just programmers -- to add roles, this will need to be dealt with.

Randy's write-up of this topic (with modifications) still hasn't migrated into the regular docs, but it certainly belongs there, and a good UI for handling roles (like 3.2.5 had) would be a great addition. I'll make a stab at this soon.

Collapse
Posted by Stan Kaufman on
In addition to creating a rel_segment for each new role and subsite, a new group_rels needs to be inserted. Otherwise the role will not show up in the role options setup by /packages/acs-subsite/www/members/member-invite.tcl. Interestingly, there isn't a tcl api for creating group_rels; is this an oversight?

More generally, I'm unclear at this point what the distinction is between rel_segments and group_rels. They both map groups to acs_rel_types, but they get used in different spots in the admin UI. Is it possible that they represent "convergent evolution" and were developed at two different points in time by two different people to accomplish the same thing? Are there important distinctions that I'm missing? Should maybe group_rels go away in favor of using exclusively rel_segments?

Collapse
Posted by Dave Bauer on
Stan

Group_rels and Group_type_rels just define a list of usable relationship types, mainly to build a UI where a user could pick the relationship type to use when adding a new user, or changing the type of a user.

Relational segments are a representation of all users which a certain relationship type to a group. So basically group_rels defines which relational segments you can add users to. It is not enforced, its just for convenience in building a user interface to add users to groups in a certain role.

Randy,

I'm a newbie in openacs. your posted message about creating new roles is very interesting..but can you tell me exactly where should i put all the codes as you mentioned before. i mean in which file or folder..

******
The process described below consists of the following steps:
1) Create Relation Types
2) Instantiate Subsite
3) Modify Default Subsite Policy
4) Create Relational Segments and Apply Privileges
5) Assign User to a Role
6) Use the OACS permissions:: System to Control Actions and Access
********
...because i've tried in different files but it doesn't work..