Julkaisukanava-hankkeen DSpace-julkaisuarkiston shibbolointi

Hanke DSpace julkaisuarkiston shibboleth-käyttäjätunnistuksen ja käyttöoikeuksien hallinnan kehittämiseen. Hanke on Tieteen tietotekniikan keskus CSC:n HAKA-pilotti. Hanke toteutetaan vuonna 2007. TTY:n hypermedialaboratorio toimii hankkeen koordinoijana ja päätoteuttajana.

Pilotin osapuolet ja toimijat (organisatorinen toimintaympäristö)

Suomen virtuaaliyliopiston asianomistajaryhmä 2:n Julkaisukanava-hanke (http://www.vy.fi/index.php?node=vp_kotisivut_vomyke_julkaisukanava_fin, osallistujat: Jyväskylän yliopisto (koordinaattori), Helsingin yliopisto, Joensuun yliopisto, Oulun yliopisto, Tampereen yliopisto, Tampereen teknillinen yliopisto Suomen virtuaaliyliopiston palveluyksikkö)

Pilotissa loppukäyttäjäksi tarkoitettu kohderyhmä

Julkaisukanava-hankkeeseen osallistuvien yliopistojen DSpace-järjestelmää käyttävät opettajat ja opiskelijat. Jatkossa mahdollisesti muutkin Suomen yliopistot.

Kohderyhmälle tarkoitetut ja toteutettavat palvelut

DSpace (http://dspace.org) digitaalinen julkaisuarkisto, johon voidaan arkistoida ja josta voidaan julkaista erilaisia sähköisiä julkaisuja kuten tieteelliset julkaisut ja digitaalisen oppimateriaalit. DSpacessa on tällä hetkellä oma autentikointimenetelmänsä. Tässä hankkeessa toteutetaan Shibboleth-tunnistautuminen DSpace-järjestelmään sekä ohjeistus Shibboloidun DSpacen käyttöönottoon, jolloin kukin yliopisto voi ottaa käyttöön Shibboleth-tunnistuksen omassa DSpace-järjestelmässään ja tarjota DSpace-järjestelmäänsä palveluksi (Service Provider) HAKA-infrastruktuuriin. Lisäksi tutkitaan mahdollisuuksia toteuttaa Shibboleth-tekniikan avulla DSpace-järjestelmän ja järjestelmään tallennettujen oppimateriaalien käyttöoikeuksien hallintaan liittyviä ominaisuuksia hyödyntäen esimerkiksi käyttäjien henkilötiedoista organisaatiotietoja (mikä yliopisto tai korkeakoulu), roolitietoja (onko opiskelija tai opettaja) ja kurssiosallistumistietoja. Yliopistojen DSpace-järjestelmiin tallennettuihin oppimateriaaleihin kohdistuviin hakuihin on tarkoitus käyttää NELLI-portaalia.

DSpacen yhdistäminen oppimisalustojen materiaalipankiksi on mahdollinen jatkokehityskohde, jossa voitaisiin hyödyntää em. käyttäjätietoihin perustuvaa käyttöoikeuksien hallintaa.

Kuvaus palveluiden teknisestä ympäristöstä sekä hahmotelma toteutuksesta

Tekninen ympäristö

DSpace on materiaalipankki/julkaisuarkisto-ohjelmisto, jota ollaan ottamassa käyttöön useassa Suomen yliopistossa mm. Julkaisukanava-hankkeen myötä. DSpace on avoimen lähdekoodin (BSD lisenssi) järjestelmä. DSpace on Javalla toteutettu ohjelmisto, jota käytetään PostgreSQL tai Oracle tietokannan päällä. Tarkoituksena on, että jokaiseen yliopistoon tulee oma DSpace-järjestelmänsä mutta tarkoituksena on myös pilotoida ratkaisua jossa kaksi yliopistoa (TaY ja TTY) käyttävät samaa DSpace-järjestelmää. Julkaisuarkisto-hankkeessa DSpaceen yhdistetään ulkoinen sopimuksenhallintajärjestelmä (jonka Shibbolointi saattaa myös tulla eteen).

Hahmotelma toteutuksesta

  1. projektisuunnitelman teko
  2. DSpace järjestelmään ja erityisesti sen autentikointi- ja authorisointimenetelmien toteutukseen tutustuminen
  3. toteutettavan järjestelmän käyttöön liittyvien toimintaprosessien määrittäminen ja dokumentointi
  4. Shibboleth-tunnistuksen toteuttaminen DSpaceen
  5. järjestelmän testaus ja pilotointi syksyllä 2007
  6. Shibboloidun DSpacen käyttöönotto-ohjeistuksen toteuttaminen
  7. raportointi. Dokumentointi tehdään CSC:n tai sen edustajan määrittelemällä tavalla projektin aikana (sisältää projektisuunnitelman, väliraportin ja loppuraportin).

Alustava arvio toteutusaikataulusta

Järjestelmän toteutus on tarkoitus tehdä kesällä 2007 ja testaus sekä pilotointi syksyllä 2007. Aikaa toteutukseen ja testaukseen arvioidaan kuluvan 2 henkilötyökuukautta.

Pilotin toteutukseen käytettäväksi suunnitellut henkilöresurssit

Pilotissa tehtävä tekninen toteutustyö sekä hankkeen sisäinen ja rahoittajan vaatima dokumentointi tehdään TTY:n hypermedialaboratoriossa yhden tutkijan ja yhden tutkimusapulaisen voimin.

Hankkeesta vastaavan henkilön yhteystiedot

Lauri Pohjanen lauri.pohjanen@tut.fi 040 - 849 0177

Dokumentaatio toteutuksesta

Määritelmät, termit ja lyhenteet

DSpace Digitaalisten aineistojen hallintajärjestelmä
HTTP HyperText Transfer Protocol
IdP Shibboleth Identity Provider
Julkaisujärjestelmä Tietojärjestelmä, jolla voidaan huolehtia sähköisesti verkko-oppimateriaalien tekijän- ja käyttöoikeuksien hallinnasta ja parantaa aineiston jakelumahdollisuuksia ja löydettävyyttä
Julkaisukanava-hanke Jyväskylän yliopiston koordinoima hanke, joka konkretisoi SVY:n strategiaa rakentamalla julkaisujärjestelmän
Manakin DSpacelle kehitetty XML -pohjainen käyttöliittymä
Shibboleth Shibboleth on väliohjelmisto organisaatiorajat ylittävään käyttäjätunnistukseen
Skeema Tiedon esitysmalli
SP Shibboleth Service Provider
TTY Tampereen teknillinen yliopisto
XML Extensible Markup Language

Shibboleth -autentikoinnin toteuttaminen DSpaceen

DSpacessa on oma autentikointimenetelmänsä, jossa tunnistaminen tapahtuu käyttäjän sähköpostiosoitteen ja salasanan perusteella. Australialaisessa MAMS-projektissa DSpaceen on toteutettu käyttäjätunnistuksen shibbolointi, jota hyödynsimme hankkeessamme.

Alkuperäiset ohjeet shibbolointiin: https://mams.melcoe.mq.edu.au/zope/mams/pubs/Installation/dspace14 DSpacen oma autentikointi on toteutettu siten, että rekisteröitynyt käyttäjä kirjautuu sisään järjestelmään sähköpostiosoitteen ja salasanan avulla. Myös MAMS -projektin shibboleth -toteutuksessa käyttäjän tunnistamiseen käytetään sähköpostiosoitetta. Sähköpostiosoite saadaan IdP -palvelimelta funetEduPerson -skeemamäärityksen mukaisesti "mail" -attribuutin arvosta. DSpace konfiguroidaan tarkastamaan tämän attribuutin arvo lisäämällä dspace.cfg -tiedoston shibboleth -asetuksiin kohdan "authentication.shib.email-header" arvoksi attribuutin nimi siinä muodossa kuin se on konfiguroitu Shibboleth SP:n AAP.xml tiedostossa:
authentication.shib.email-header = SHIB_MAIL

eduPersonPrincipalName -attribuutti

Sähköpostiosoitteen sijaan suositeltavampi vaihtoehto käyttäjän tunnistamiseen on funetEduPerson -skeeman "eduPersonPrincipalName" -attribuutti. Jos tämä vaihtoehto halutaan käyttöön, tulee shib-dspace14.patch -tiedoston asentamisen jälkeen tehdä muutoksia dspace/src/au/edu/mq/melcoe/mams/dspace/eperson/ShibAuthentication.java -tiedostoon. DSpacen tietokannassa "eperson" -taulussa on valmiina sarake "netid" LDAP -autentikointia varten. Jos LDAP -tunnistamista ei käytetä, voidaan kyseistä saraketta käyttää "eduPersonPrincipalName" -attribuutin arvon säilömiseen. DSpacen EPerson -luokassa on myös valmiina metodit "netid" -muuttujan käsittelyyn. Vaadittavat muutokset saadaan tehtyä seuraavasti:
  • Asenna patch, joka tuottaa muutoksia ShibAuthentication -luokan authenticate -metodiin.
  • Lisää dspace.cfg -tiedostoon shibboleth -asetusten yhteyteen seuraavat tiedot (korjaa tarvittaessa arvot kuten ne on konfiguroitu Shibboleth SP:n AAP.xml tiedostossa):
authentication.shib.principalname-header = SHIB_EP_PRINCIPALNAME
authentication.shib.email-header = SHIB_MAIL
authentication.shib.surname-header = SHIB_EP_SN
authentication.shib.givenname-header = SHIB_EP_GIVENNAME

Näiden muutosten jälkeen järjestelmä osaa myös poimia ja tallentaa shibboleth -attribuuteista käyttäjän etunimen ja sukunimen tämän kirjautuessa palveluun ensimmäistä kertaa.

Muu konfigurointi

  • Varmista, että DSpacen ohjaus Apachelle tapahtuu joko JK2:n tai JK:n kautta (jälkimmäinen olettaa, että dspace on nimetty "dspace"). Tämä vaaditaan, koska toistaiseksi Shibboleth SP:lle on olemassa ainoastaan Apache -moduuli. JK:n tapauksessa, lisää tarvittaessa seuraavat rivit tiedostoon /etc/httpd/conf.d/mod_jk.conf (tai vastaavanlaiseen):
JkMount /dspace/* ajp13
JkMount /manakin/* ajp13
  • Lisää Shibboleth SP -suojaus DSpaceen ja Manakiniin lisäämällä /etc/httpd/conf.d/shib.conf -tiedostoon (tai vastaavanlaiseen) seuraavat merkinnät:
<Location /dspace/shibboleth-login>
    AuthType shibboleth
    ShibRequireSession On
    require valid-user
</Location>

<Location /manakin/shibboleth-login>
    AuthType shibboleth
    ShibRequireSession On
    require valid-user
</Location>
  • Kun kaikki ylläoleva on tehty, käynnistä uudelleen Apache ja Tomcat. Testaa DSpace asennuksesi valitsemalla DSpacessa "My DSpace" -toiminto tai Manakinissa "Login" -toiminto, jolloin voit kokeilla Shibboleth -kirjautumista.

Toimivuus Manakinissa

Manakin käyttöliittymässä on versiosta 1.1 lähtien valmiiksi toteutettuna tuki shibboleth-kirjautumiselle, joten Manakinin lähdekoodeihin ei erikseen tarvita muutoksia. Kuitenkin jos shibboleth-tekniikaa on tarkoitus hyödyntää autentikoinnin lisäksi myös auktorisointiin, tarvitaan muutama pieni muutos DSpacen ja Manakinin lähdekoodeihin. DSpacen "au.edu.mq.melcoe.mams.dspace.app.webui.servlet.ShibbolethServlet" -luokan "fixGroupMembership" -funktion private -määreen tilalle täytyy muuttaa static public ja Manakinin tiedostoon org.dspace.app.xmlui.utils.AuthenticationUtil.java tulee lisätä seuraavat rivit:
import au.edu.mq.melcoe.mams.dspace.app.webui.servlet.ShibbolethServlet; // rivi 56
    ...
ShibbolethServlet.fixGroupMembership(context,request); // rivi 179 (if-rakenteen sisään)

Käyttäjän roolit ja käyttöoikeuksien hallinta DSpacessa

Hankkeessa tutkittiin, miten käyttäjien auktorisointi ja käyttöoikeuksien hallinta käytännössä toimii DSpacessa. Käyttäjille ("EPeople") ja ryhmille ("Groups") voidaan myöntää DSpacessa erilaisia oikeuksia. Järjestelmässä on olemassa kaksi erityisryhmää: ylläpitäjä ("Administrator"), jolla on oikeudet kaikenlaisiin järjestelmän resursseihin ja toimintoihin sekä anonyymi käyttäjä ("Anonymous"), johon kuuluu kaikki käyttäjät. Myöntämällä "Anonymous" -ryhmälle pääsy johonkin resurssiin tarkoittaa, että kenellä tahansa on pääsy kyseiseen resurssiin. Alla on lueteltu käyttöoikeudet DSpacen eri resursseihin.
Yhteisö (Community)
ADD/REMOVE kokoelmien ja alikokoelmien lisääminen tai poistaminen
Kokoelma (Collection)
ADD/REMOVE julkaisujen lisääminen tai poistaminen
DEFAULT_ITEM_READ julkaisujen näyttäminen
DEFAULT_BITSTREAM_READ julkaisujen tiedostojen näyttäminen
COLLECTION_ADMIN kokoelman ylläpitäjä voi mm. muokata kokoelman julkaisujen tietoja, hyväksyä julkaisuja kokoelmaan sekä poistaa niitä kokoelmasta
Julkaisu (Item)
ADD/REMOVE tiedostoryhmien lisääminen tai poistaminen
READ julkaisujen näyttäminen (julkaisun kuvailutiedot (metadata) näkyy aina kaikille)
WRITE julkaisun tietojen muokkaaminen
Tiedostoryhmä (Bundle)
ADD/REMOVE tiedostojen lisääminen tiedostoryhmään tai poistaminen ryhmästä
Tiedosto (Bitstream)
READ näyttäminen
WRITE muokkaaminen

Ryhmän perustaminen

DSpace Manakinissa järjestelmän ylläpitäjä voi luoda uuden ryhmän valitsemalla "Administrative" -valikosta Groups -linkin. Tällöin aukeaa "Group management" -sivu, josta valitaan "Click here to add a new Group" -linkki. Päädytään "Group Editor" -sivulle, jossa annetaan perustettavalle ryhmälle nimi ja haluttaessa lisätään ryhmään käyttäjiä tai toisia ryhmiä jäseniksi.

Kokoelmakohtaiset auktorisoidut perusryhmät

DSpacessa kokoelmiin liittyy tiettyjä auktorisoituja rooleja ja kutakin roolia vastaa erityinen automaattisesti nimetty ryhmä. Alla olevassa taulukossa on lueteltu nämä kokoelmakohtaiset roolit sekä niitä vastaavien ryhmien nimet kokoelmalle 1 (kokoelman ID = 1). Perustettaessa uusi kokoelma nämä perusryhmät ovat oletuksena poissa käytöstä. Otettaessa esimerkiksi käyttöön yksi tai useampi ryhmä kolmesta WORKFLOW_STEP -ryhmästä tarkoittaa, että käyttäjien lisäämiä julkaisuja ei julkaista kokoelmassa suoraan, vaan uusien julkaisujen tulee saada hyväksyntä tarkistukseen oikeutetun henkilön toimesta.
Rooli Ryhmä  
Kokoelman ylläpitäjä COLLECTION_1_ADMIN Kokoelmien ylläpitäjät voivat poistaa julkaisuja kokoelmasta, muokata julkaisujen kuvailutietoja, lisätä (mapata) julkaisuja muista kokoelmista kokoelmaan sekä päättää, ketkä käyttäjät voivat lisätä julkaisuja kokoelmaan.
Hyväksymis-/ hylkäämisaskel COLLECTION_1_WORKFLOW_STEP_1 Henkilöt, jotka ovat vastuussa tästä askeleesta, voivat hyväksyä tai hylätä käyttäjien lisäämiä hyväksyntää odottavia julkaisuja, mutta eivät voi muokata julkaisujen tietoja.
Kuvailutietojen (metadatan) hyväksymis-/ hylkäämis-/ muokkaus-askel COLLECTION_1_WORKFLOW_STEP_2 Henkilöt, jotka ovat vastuussa tästä askeleesta, voivat muokata hyväksymisvaiheessa olevien julkaisujen kuvailutietoja sekä hyväksyä tai hylätä julkaisuja.
Kuvailutietojen muokkaus -askel COLLECTION_1_WORKFLOW_STEP_3 Henkilöt, jotka ovat vastuussa tästä askeleesta, voivat muokata hyväksymisvaiheessa olevien julkaisujen kuvailutietoja, mutta eivät voi hylätä julkaisuja.
Aineistojen lisäys COLLECTION_1_SUBMIT Käyttäjät ja ryhmät, joilla on oikeus tallentaa uusia julkaisuja (odottamaan hyväksyntää) kokoelmaan.
Lukuoikeus COLLECTION_1_DEFAULT_READ Käyttäjät ja ryhmät, jolla on lukuoikeus kokoelman uusiin julkaisuihin. Muutokset tähän rooliin eivät vaikuta taannehtivasti, joten vanhat julkaisut ovat edelleen luettavissa niille, joilla oli lukuoikeus julkaisujen lisäämishetkellä.

Käyttöoikeuksien lisääminen ja poistaminen

Uusia käyttöoikeuksia erilaisiin resursseihin tai toimintoihin voivat asettaa ainoastaan järjestelmän ylläpitäjät. Kokoelmien ylläpitäjien sallitaan lisätä uusia käyttäjiä tai ryhmiä jäseniksi ylläpitämiensä kokoelmien käytössä oleviin perusryhmiin sekä poistaa käyttäjiä tai ryhmiä näistä ryhmistä. Poikkeuksena on DEFAULT_READ -ryhmä, jonka jäseniä kokoelman ylläpitäjä ei voi lisätä tai poistaa. Perustaessaan uutta kokoelmaa järjestelmän ylläpitäjä voi asettaa oikeudet kokoelmaan "Assign Roles" -välilehdellä, joka tulee esiin kokoelman metatietojen syöttämisen jälkeen. Saman välilehden järjestelmän tai kokoelman ylläpitäjä saa myös esiin valitsemalla yhteisöt ja kokoelmat -listalta halutun kokoelman, jolloin "My Account" -valikon alapuolelle tulee näkyviin "Context" -valikko, josta valitaan "Edit Collection" -linkki.

"Assign Roles" -sivulla on listattuna yllä olevassa taulukossa esitetyt perusryhmät ja toiminnot niiden käyttöönottoon tai poistamiseen. Kunkin käyttöön otetun ryhmän kohdalla on linkki, josta pääsee lisäämään tai poistamaan käyttäjiä tai ryhmiä. Järjestelmän ylläpitäjän tapauksessa sivun alareunassa on linkki "Edit authorization policies directly", josta aukeaa sivu, jossa on listattuna kaikki kokoelman käyttöoikeudet ja jossa oikeuksia voidaan lisätä ja poistaa yksitellen. Tämän sivun järjestelmän ylläpitäjä saa esiin myös suoraan valitsemalla "Administrative" -valikosta "Authorizations" -toiminnon ja valitsemalla esiin tulevasta listasta kokoelman.

Kurssiosallistumistiedon tunnistaminen

Käytettäessä DSpace -järjestelmää digitaalisten oppimateriaalien hallintaan, saattaa ilmetä tarvetta rajoittaa esimerkiksi jonkin kurssin oppimateriaalien lukuoikeudet ainoastaan kurssin opiskelijoille siten, että ulkopuoliset käyttäjät eivät pääse lukemaan materiaalien sisältöjä. Tällöin on hyödyllistä pystyä tunnistamaan kurssilaiset automaattisesti Shibboleth -tekniikkaa hyödyntäen IdP -palvelimen tarjoamien tietojen perusteella.

Kurssiosallistumistiedon tarjoava attribuutti

FunetEduPerson 2.0 -skeeman määrittelyssä neuvotaan korkeakouluja seuraamaan Internet2:n eduCourse -skeeman kehitystä kurssitietoihin ja kurssiosallistumistietoihin liittyen. TTY:n IdP tarjoaa urn:mace:funet.fi:tut.fi:attribute-def:eduCourseMember -nimisen attribuutin, jonka arvona on lista käyttäjän voimassaolevista kurssiosallistumisista. Listan alkiot ovat muotoa learner@urn:mace:funet.fi:tut.fi:courses:MATHM-47050:2006-2-1, missä "@" -merkkiä edeltävä osa ilmoittaa henkilön roolin kurssilla ja loppuosa on kurssin yksilöivä tunniste. Roolin määrittävän merkkijonon mahdollisiksi arvoiksi eduCourse luettelee "Learner", "Instructor", "ContentDeveloper", "Member", "Manager", "Mentor", "Administrator" ja "TeachingAssistant", mutta nämä saattavat vaihdella oppilaitoksesta riippuen. Esimerkissämme urn:mace:funet.fi:tut.fi on Tampereen teknillisen yliopiston yksilöllinen tunniste ja MATHM-47050 opintojakson korkeakoulukohtainen yksilöllinen tunniste. Numerosarja 2006-2-1 tarkoittaa, että kyseessä on lukuvuoden 2006-2007 toisen periodin ensimmäinen kurssi. Emme tutustuneet muiden korkeakoulujen IdP -palvelimien tarjoamiin kurssiosallistumisattribuutteihin.

Kurssikohtaisen ryhmän nimeämiskäytäntö

Kun halutaan rajoittaa lukuoikeus kurssin materiaaleihin kaikkien opiskelijoiden sijaan ainoastaan kurssin opiskelijoille, tulee DSpaceen luoda ryhmä, jolle myönnetään lukuoikeus kurssin materiaalikokoelmaan. Jos DSpace -järjestelmä on korkeakoulukohtainen tai järjestelmässä on korkeakoulukohtaiset yhteisöt siten, että vain oman korkeakoulun opiskelijoilla on pääsy järjestelmään tai yhteisöön, voidaan ryhmä nimetä esimerkiksi seuraavan mallin mukaisesti: MATHM-47050:2006-2-1 eli opintojakson tunnisteen ja kurssin toteutusajankohdan mukaisesti. Jos halutaan, että oppimateriaalit ovat käytettävissä kaikilla opistojakson kursseilla lukuvuodesta ja periodista riippumatta, voidaan ryhmä nimetä pelkän opintojakson tunnisteen mukaan (esim. MATHM-47050).

Järjestelmän ollessa avoin siten, että korkeakoulujen tunnuksilla pääsee kirjautumaan toisten organisaatioiden oppimateriaalijärjestelmiin, on ryhmän nimeen tarpeellista lisätä opintojakson tunnisteen lisäksi korkeakoulun tunniste, koska eri korkeakouluissa saattaa olla samanlaiset opintojaksotunnisteet käytössä. Käytämme toteutuksessamme seuraavanmuotoisia ryhmien nimiä: tut.fi:MATHM-47050:2006-2-1 ja tut.fi:MATHM-47050. Ryhmät voitaisiin nimetä myös esimerkiksi IdP-attribuutin arvon kaltaisiksi ( learner@urn:mace:funet.fi:tut.fi:courses:MATHM-47050:2006-2-1), mutta lyhyempi nimi lienee käyttökelpoisempi.
Kattavuus Nimeäminen Esimerkki
Kurssi tut.fi:opintojaksotunnus:lukuvuosi-periodi-toteutuskerta tut.fi:MATHM-47050:2006-2-1
Opinto-jakso tut.fi:opintojaksotunnus tut.fi:MATHM-47050

Tunnistamisen toteuttaminen DSpaceen

Tunnistamisen toteuttaminen Shibboloituun DSpaceen oli melko vaivatonta. DSpacen dspace.cfg -tiedoston Shibboleth -asetuksiin kohtaan "authentication.shib.role-header" täytyy lisätä attribuutin nimi siinä muodossa kuin se on konfiguroitu Shibboleth SP:n AAP.xml tiedostoon:
authentication.shib.role-header = SHIB_FEP_COURSEMEMBER

Muita muutoksia ei konfiguraatiotiedostoon tarvita.

DSpacen au.edu.mq.melcoe.mams.dspace.eperson.ShibAuthentication -luokan kohta:
String[] labels = groupLabels.split(","); // rivi 173
for (int i = 0; i < labels.length; i++)
    addGroup(groups, context, labels[i].trim());

pitää korvata seuraavilla riveillä:
if ( groupLabels.startsWith("learner@urn") )
{
    String[] parts = groupLabels.split(":");
    if (parts.length == 7)
    {
   String label = parts[3] + ":" + parts[5]; // esim: tut.fi:MATHM-47050
   addGroup(groups, context, label);
   addGroup(groups, context, label + ":" + parts[6]);
    }
}
else
{
     String[] labels = groupLabels.split(",");
     for (int i = 0; i < labels.length; i++)
     addGroup(groups, context, labels[i].trim());
}

Edellä esitettyjen muutosten toteuttamisen jälkeen järjestelmä pyrkii linkittämään Shibboleth-kirjautumisen yhteydessä käyttäjän sekä opintojaksokohtaiseen ( tut.fi:MATHM-47050) että kurssikohtaiseen ( tut.fi:MATHM-47050:2006-2-1) auktorisoituun ryhmään. Linkittäminen tapahtuu ainoastaan siinä tapauksissa, että järjestelmässä on luotuna täsmälleen nimeämisperiaatteen mukaan nimetty kurssia tai opintojaksoa vastaava ryhmä.

Järjestelmän voisi ohjelmoida tunnistamaan automaattisesti myös esimerkiksi kurssin vastuuhenkilön tai kurssiassistentin, jotka voitaisiin kirjautumisen yhteydessä linkittää sellaiseen ryhmään, jolla on ylläpito-oikeudet kurssin materiaalikokoelmaan. Tällaisen ryhmän nimi voisi olla esimerkiksi muotoa admin@tut.fi:MATHM-47050:2006-2-1. Emme kokeilleet käytännössä toteuttaa tällaista tapausta, mutta toteuttaminen olisi varmasti varsin vaivatonta.

Jatko-opiskelijaroolin tunnistaminen

Hankkeen puitteissa myös testattiin TTY:n omiin mahdollisiin tarpeisiin liittyen mahdollisuutta tunnistaa käyttäjä automaattisesti Shibboleth -kirjautumisen yhteydessä jatko-opiskelijaksi. Ideana oli rajoittaa oikeus lisätä julkaisuja DSpaceen perustettuun Väitöskirjat -kokoelmaan ainoastaan jatko-opiskelijoille. Automaattisen tunnistamisen ansiosta järjestelmän ylläpitäjien ei tarvitse manuaalisesti yksitellen lisätä opiskelijoille oikeuksia väitöskirjan lisäämiseen.

funetEduPersonTargetDegreeUniversity -attribuutti

Jatko-opiskelijaroolitiedon toteamiseen käytettiin funetEduPerson 1.0 -skeeman "funetEduPersonTargetDegreeUniversity" -attribuuttia. Tämän attribuutin arvona oleva numerokoodi ilmoittaa, mitä tutkintoa käyttäjä on suorittamassa korkeakoulussa. Tilastokeskus ylläpitää tietoja korkeakoulukohtaisista tiedekunta-, tutkinto-, koulutusohjelma- ja pääainekoodistoista. Esimerkiksi TTY:ssa koodi "110" tarkoittaa tekniikan tohtoria ja koodi "704" filosofian tohtoria. Emme testanneet käytännössä, miten laajasti attribuutti on käytössä ja tarjotaan eri korkeakoulujen IdP -palvelimilta. Uudemmassa funetEduPerson 2.0 -skeemamäärityksessä attribuutti on korvattu "funetEduPersonTargetDegree" -nimisellä attribuutilla, jollaista käyttämämme IdP -palvelin ei tarjonnut.

DSpacen konfigurointi

Tunnistamisen toteuttaminen Shibboloituun DSpaceen oli melko yksinkertaista toteuttaa. Pieniä muutoksia tarvittiin dspace.cfg -konfiguraatiotiedostoon. Tiedoston Shibboleth -asetuksiin kohtaan "authentication.shib.role-header" täytyy lisätä attribuutin nimi siinä muodossa kuin se on konfiguroitu Shibboleth SP:n AAP.xml tiedostoon:
authentication.shib.role-header = SHIB_FEP_TARGETDEGREEUNIVERSITY

Lisäksi asetuksiin lisätään kutakin attribuutin hyväksyttävää roolitietoa – eli tohtorintutkinnon numerokoodia – kohden DSpacesta löytyvän ryhmän nimi. Toteutuksessamme "TargetDegreeUniversity" -attribuutin sisältäessä arvon "110" tai "704" tulkitaan käyttäjä jatko-opiskelijaksi. Kun DSpaceen on lisätty Jatko-opiskelija -niminen ryhmä, jolle on myönnetty Submit -oikeus Väitöskirjat -kokoelmaan, tulee konfiguraatiotiedostoon lisätä seuraavankaltaiset rivit:
authentication.shib.role.110 = Jatko-opiskelija
authentication.shib.role.704 = Jatko-opiskelija

Vaihtoehtoisia attribuutteja jatko-opiskelijaroolin tunnistamiseen

Käyttökelpoinen vaihtoehto jatko-opiskelijaroolitiedon toteamiseen olisi varmasti ollut funetEduPerson 2.0 -skeeman "funetEduPersonStudentCategory" -attribuutti, joka ilmoittaa opiskelijan kategorian tavoitetutkinnon mukaan. Tämän attribuutin laillisia arvoja ovat mm. "bachelor", "master", "licentiate", "doctor" ja "exchange-student", joten jatko-opiskelijan tunnistaminen on suoraviivaista. Käyttämämme IdP ei kuitenkaan tarjonnut tätä attribuuttia.

Korkeakoulujen IdP -palvelimet saattavat tarjota lisäksi omia attribuutteja, joiden arvoista jatko-opiskelijaroolitieto voidaan päätellä. Esimerkiksi käyttämämme IdP tarjosi urn:mace:funet.fi:tut.fi:attribute-def:role -attribuutin, jonka arvoista olisi voinut saada selville, että onko käyttäjä jatko-opiskelija. Tätä attribuuttia emme kuitenkaan käyttäneet, koska selkeämmin roolitieto saatiin selville urn:mace:funet.fi:attribute-def:funetEduPersonTargetDegreeUniversity -attribuutista.

Käyttöoikeussopimukset ja auktorisointi

Julkaisukanava-hankkeessa käytetään Dr.M -sopimustenhallintaohjelmistoa, jonka toiminta-ajatuksena on digitalisoida erilaisten sopimusten ja niiden sisältämän tiedon tuottaminen, käsittely, arkistointi ja hakeminen. Sopimukset arkistoidaan ohjelmiston avulla sekä formaalilla (XML) kielellä, että luonnollisella ihmisten tulkittavissa olevalla kielellä. Käyttöoikeussopimus on kahden tai useamman tahon välinen ja siinä voi olla määritelty esimerkiksi julkaistavan digitaalisen materiaalin käytön laajuus. Tekijä voi sallia esimerkiksi materiaalin käytön yksittäisessä korkeakoulussa tai vaihtoehtoisesti esimerkiksi ainoastaan korkeakoulun yksittäisellä kurssilla. DSpace voidaan liittää ohjelmistoon rajapinnan avulla. Sopimusjärjestelmään liitetyssä DSpace -järjestelmässä julkaisusopimusten tekeminen on mahdollista julkaisuprosessin yhtenä vaiheena. Esimerkki sopimuksen tekemisestä DSpacessa löytyy Julkaisukanava-hankkeen esittelymateriaalista.

Rajapinnan asentaminen DSpaceen

Dr.M -sopimustenhallintaohjelmisto koostuu selainkäyttöliittymästä, Microsoft Wordiin asennettavista työkaluista sekä palvelinohjelmistosta, jonka yhteydessä toimivaan tietokantaan ohjelmiston käsittelemät tiedot tallennetaan. Käytimme testaukseen Jyväskylän yliopiston kirjastoon (JYK) asennettua testijärjestelmää. Emme siis asentaneet kokonaista ohjelmistoa omaan palvelinympäristöömme, vaan ainoastaan rajapinnan DSpace/Manakin -järjestelmämme yhteyteen. Asentamiseen tarvittavat tiedostot ja ohjeet saimme JYK:sta.

Sopimusmalli

Jotta sopimuksia voidaan tehdä DSpacessa, tarvitaan sopimusmalli. Mallin kirjoittaminen tapahtuu sopimusjärjestelmän Microsoft Word -ylläpitokäyttöliittymässä. Yleensä sopimusmallissa on mm. kohdat osapuolille ja kohteelle sekä sopimustekstiä. Mallin pohjalta saadaan pienin täydennyksin ja muokkauksin muodostettua varsinainen sopimus. Jokaisella sopimusjärjestelmään tallennetulla sopimusmallilla on yksilöllinen urn-tunniste. DSpacessa sopimusmallin linkittäminen kokoelmaan tapahtuu lisäämällä sopimusmallin urn-tunniste kokoelman metatietoihin "License" -kohtaan. Osa sopimukseen tulevista tiedoista voidaan DSpacessa poimia suoraan julkaistavan materiaalin metatiedoista ja loput tarvittavat tiedot syötetään lomakkeeseen, joka muodostetaan automaattisesti sopimusmallin pohjalta.

Testitoteutuksessamme sopimukseen tulevat tekijän, julkaisijan ja julkaisun nimi poimitaan julkaisun metatiedoista ( dc.creator, dc.publisher, dc.title), jotka syötetään julkaisuprosessissa ennen sopimuksentekoaskelta. Sopimusmallin pohjalta muodostetussa sopimuslomakkeessa on valintanappi (radio button) -lista, jossa vaihtoehtoina materiaalin käytön laajuudelle ovat korkeakoulu ja kurssi. Korkeakoulun nimi ja tunnus tai kurssin nimi ja tunnus syötetään lomakkeeseen. Jos esimerkiksi lomakkeesta valitaan "Käyttö kurssilla" -vaihtoehto ja kurssin nimeksi syötetään "Saavutettavuus" ja kurssin tunnukseksi "MATHM-47050", tulee sopimukseen teksti: "Julkaisija saa käyttää teosta TTY:n opetuksessa seuraavalla kurssilla: Saavutettavuus, kurssikoodi MATHM-47050" (tai vastaavanlainen teksti).

Käyttöoikeuksien asettaminen sopimustietojen perusteella

Toteutimme järjestelmän toimimaan siten, että käyttöoikeussopimuksen tietoja verrataan shibbolethin kautta saataviin tietoihin ja tämän perusteella sallitaan tai estetään käyttäjän pääsy aineistoon. Käytännössä XML -muotoisesta sopimuksesta poimitaan esimerkiksi kurssin tunnus tai korkeakoulun tunnus ja etsitään järjestelmästä tämän niminen ryhmä. Jos ryhmää ei ole olemassa, perustetaan sellainen. Ryhmälle annetaan lukuoikeus materiaaliin. Käyttäjän kirjautuessa sisään tarkistetaan shibboleth-attribuuttien arvoista, voidaanko käyttäjä liittää ryhmään.

Yksi tai useampi kolmesta kokoelman WORKFLOW_STEP -roolista (ks. Kohta 2.3.2) on hyvä olla käytössä kokoelmassa, johon liittyy sopimuksentekoaskel. Julkaisun tarkastajalle tai ylläpitäjälle näkyy julkaisun metatietosivun alareunassa linkki "Show item's contracts", josta saa esiin listan kokoelmaan liittyvistä sopimuksista. Valitsemalla listasta haluttu sopimus saadaan näkyviin sivu, jossa voidaan mm. muuttaa sopimuksen tilaa. Seuraavat tilat ovat mahdollisia:
  • Valid (Voimassa oleva)
  • Waiting (Odotus)
  • Void (Mitätöity)
  • Deleted (Poistettu)
Aluksi tilana on ”Waiting”. Testijärjestelmämme toimii siten, että muutettaessa tilaksi ”Valid”, asetetaan sopimuslomakkeesta saadulle ryhmälle (kurssin tai korkeakoulun tunnus) lukuoikeudet julkaisun sisältöön. Kun tilaksi muutetaan ”Waiting”, ”Void” tai ”Deleted” asetetaan julkaisuun kokoelman oletuskäyttöoikeudet. Tämän vuoksi kokoelman oletusoikeudet kannattanee määritellä esim. siten, kaikilla käyttäjillä on pääsy julkaisujen metatietoihin, mutta julkaisujen sisältöön (tiedostoihin) pääsy on rajoitettu. Emme ota tässä kantaa siihen, miten käyttöoikeudet päivitetään silloin, kun sopimuksen tilaa muutetaan DSpacen ulkopuolella (esim. sopimustenhallintaohjelmiston ylläpitokäyttöliittymässä).

Toteutuksessamme sopimuksesta poimitaan tarvittava tieto (ryhmän nimi) jo sopimuksentekovaiheessa ja se tallennetaan DSpacen tietokantaan julkaisun (item) tietojen yhteyteen. Parempi vaihtoehto varmasti olisi poimia tieto suoraan sopimuksesta siinä vaiheessa, kun sopimuksen tila muutetaan voimassaolevaksi. Sopimuksenhallintaohjelmiston DSpace-rajapinta ei kuitenkaan tällä hetkellä tarjoa mahdollisuutta päästä käsiksi sopimuksen tietoihin muutoin kuin sopimuksentekovaiheessa.

Auktorisoinnin toteuttaminen

Tässä kohdassa on esitettynä yksityiskohtaisesti kaikki ne muutokset, jotka teimme järjestelmään, jotta saimme sen toimimaan edellä kuvatulla tavalla. Tietokantaan tarvitaan yksi muutos. Uusi sarake nimeltään allowed_group pitää lisätä item -tauluun ryhmän tunnistetta (ID) varten.
ALTER TABLE item ADD allowed_group INTEGER;

Luokkaan org.dspace.content.Item tulee lisätä seuraavat metodit ryhmien ja oikeuksien käsittelyä varten. allowGroup -metodin avulla asetetaan ryhmälle lukuoikeus julkaisun sisältöön.
/**
 * Get the group that is allowed to access this item (according to the contract)
 * 
 * @return the group
 */
public Group getAllowedGroup() throws SQLException
{
    Group group = null;
    if (!itemRow.isColumnNull("allowed_group"))
    {
        group = Group.find(ourContext, itemRow.getIntColumn("allowed_group"));
    }
    return group;
}

/**
 * Set the group that is allowed to access this item (according to the contract)
 * 
 * @param group
 *            the group
 */
public void setAllowedGroup(Group group)
{
    if (group != null)
    {
        itemRow.setColumn("allowed_group", group.getID());
    }
    else
    {
        itemRow.setColumnNull("allowed_group");
    }
}
    
/**
 * Remove all of the policies for item's bitstreams and bundles and add new policies
 * to allow AllowedGroup (that is given in the column "allowed_group") to access the
 * contents of the item
 * 
 */
public void allowGroup() throws SQLException, AuthorizeException
{
    Group group = getAllowedGroup();
    if (group != null)
    {
        // add new policy
        ResourcePolicy policy = ResourcePolicy.create(ourContext);
        policy.setAction(Constants.READ);
        policy.setGroup(group);
        policy.update();
        List newpolicies = new LinkedList();
        newpolicies.add(policy);
        replaceAllBitstreamPolicies(newpolicies);
    }
}

Jotta uuden ryhmän perustaminen onnistuu ilman ylläpitäjän oikeuksia, tulee seuraava kohta poistaa org.dspace.eperson.Group -luokan create -funktiosta:
if (!AuthorizeManager.isAdmin(context))
{
    throw new AuthorizeException("You must be an admin to create an EPerson Group");
}

Tiedostoon fi.vy.vomyke.drm.drmContractStepManakin.java lisätään seuraavat importit:
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Document;
import org.jdom.Attribute;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
import org.jdom.filter.ElementFilter;
import org.jdom.xpath.XPath;
import org.dspace.eperson.Group;

Funktioon processLicense pitää tehdä joitakin lisäyksiä, jotta kurssikoodi tai korkeakoulun tunnus saadaan poimittua sopimuksesta ja tallennettua julkaisun (item) tietoihin. Lisäysten jälkeen funktion alkuosa tulisi olla seuraavanlainen:
private int processLicense(Context context, HttpServletRequest request, HttpServletResponse response, SubmissionInfo subInfo)
throws ServletException, IOException, SQLException, AuthorizeException
{
    log.debug("Processing license in drmContractStepManakin...");

    String contractModelViewMode = (String) request.getParameter("contractModelViewMode");
    String buttonPressed = UIUtil.getSubmitButton(request, CANCEL_BUTTON);
    String decision = request.getParameter("decision");

    log.debug("buttonPressed = "+buttonPressed);

    boolean licenseGranted = false;

    // OPTION 1
    // Accepting the license means checking a box and clicking Next
    if (decision != null && decision.equalsIgnoreCase("accept")
          && buttonPressed.equals(NEXT_BUTTON))
    {
        log.debug("doing the grant...");

        // Lisätty
        String groupName = null;
         
        // Add the license to the item
        Item item = subInfo.getSubmissionItem().getItem();
        try {
            DRMContract cont = (DRMContract)request.getSession().getAttribute("license");
            
            String contractURN = cont.archiveContract();
            item.addDC("rights", "uri", null, contractURN);            
            
            // Lisätty
            // find course code or organization code from contract
            try {
                String xml = cont.getEditableContentXML();
                log.debug("xml contract\n" + xml);
                StringReader reader = new StringReader(xml);
                Document document = new SAXBuilder().build(reader);
                int selection = findScopeSelection(document.getRootElement());

                if (selection != -1)
                {
                    groupName = findScopeValue(document.getRootElement(), selection);
                }
                  
            } catch(Exception e) { 
                log.error("Error in getting values from contract: "+e);
            }

            request.getSession().setAttribute("license", null); 
            context.commit();
            log.debug("New contract archived, URN = "+contractURN);
        } catch (Exception e) { System.out.println("Error in  SubmitServletWithDRM.processLicense / preview contract: " + e);}

        // Lisätty
        // add groupname to the workspaceItem
        if (groupName != null && groupName.length() > 0)
        {
            log.debug("groupName: " + groupName);
            
            // find group or create new one if not found
            Group group = Group.findByName(context, groupName);
            if (group == null)
            {
                group = Group.create(context);
                group.setName(groupName);
                group.update();
            }
            
            // Lisätty
            WorkspaceItem wi = (WorkspaceItem) subInfo.getSubmissionItem();
            wi.getItem().setAllowedGroup(group);
            wi.getItem().update();
        }

        //commit changes. Do we need this???
        context.commit();
    }

Sopimuksen tiedot saadaan sopimustenhallintajärjestelmästä seuraavanlaisessa muodossa:
<?xml version="1.0" encoding="UTF-8"?>
<TemplateItems>
  <DCInputs>
    <DCTextInput>
      <DCName>dc.title</DCName>
      <WordName>DrMInput2</WordName>
    </DCTextInput>
    <DCTextInput>
      <DCName>dc.publisher</DCName>
      <WordName>DrMInput0</WordName>
    </DCTextInput>
    <DCTextInput>
      <DCName>dc.creator</DCName>
      <WordName>DrMInput1</WordName>
    </DCTextInput>
  </DCInputs>
  <DrMInputs>
    <RadioGroup>
      <Radios>
        <RadioButton><Inputs>
            <StringInput><Name>Korkeakoulun nimi</Name><WordName>DrMInput3</WordName><Required>false</Required><Value></Value></StringInput>
            <StringInput><Name>Korkeakoulun tunnus</Name><WordName>DrMInput4</WordName><Required>false</Required><Value></Value></StringInput>
          </Inputs><Name>K�yttö korkeakoulussa</Name><WordName>DrMSelectionRadioButton0</WordName></RadioButton>
        <RadioButton><Inputs>
            <StringInput><Name>Kurssin nimi</Name><WordName>DrMInput5</WordName><Required>false</Required><Value>Saavutettavuus</Value></StringInput>
            <StringInput><Name>Kurssikoodi</Name><WordName>DrMInput6</WordName><Required>false</Required><Value>MATHM-47050</Value></StringInput>
          </Inputs><Name>K�yttö kurssilla</Name><WordName>DrMSelectionRadioButton1</WordName><DefaultCheck>true</DefaultCheck></RadioButton>
      </Radios>
      <WordName>K�ytön laajuus</WordName>
    </RadioGroup>
    <DateInput><Name>AllekirjoituspÃ?ivÃ?mÃ?Ã?rÃ?</Name><WordName>DrMInput7</WordName><Required>false</Required><Value>31.8.2007</Value></DateInput>
  </DrMInputs>
</TemplateItems>

Luokkaan fi.vy.vomyke.drm.drmContractStepManakin pitää lisätä seuraavankaltaiset metodit, joiden avulla etsitään ylläolevankaltaisesta XML-rakenteesta käyttäjän valitsema vaihtoehto sekä kurssin tai korkeakoulun tunnus:
/**
 * Find course code or organization code from xml document
 * Note: This is customized for a specific contract template!!
 * 
 * @param currentElement
 * @param selection
 * @return the code which was found
 */
public String findScopeValue(Element currentElement, int selection)
{
    try {
        if (currentElement.getName().equals("StringInput"))
        {
            log.debug("Name: "+currentElement.getChildText("Name"));
            if (selection == 0 && currentElement.getChildText("Name").equals("Korkeakoulun tunnus"))
            {
                String org = currentElement.getChildTextTrim("Value");
                if ( org != null && !org.isEmpty() )
                {
                    // Taman saa poistaa sitten kun lomake on muutettu kayttokelpoisemmaksi
                    if ( org.toLowerCase().equals("tty") 
                         ||  org.toLowerCase().equals("tut") 
                         || org.toLowerCase().equals("tut.fi"))
                    {
                        return "tut.fi";
                    }
                    return org;
                }   
            }
            else if (selection == 1 && currentElement.getChildText("Name").equals("Kurssikoodi"))
            {
                String courseCode = currentElement.getChildTextTrim("Value");
                if ( courseCode != null && !courseCode.isEmpty() )
                {
                    return "tut.fi:" + courseCode;
                }   
            }
        }

        Iterator itr = (currentElement.getChildren()).iterator();
        while(itr.hasNext()) {
            Element child = (Element)itr.next();
            String result = findScopeValue(child, selection);
            if(result != null)
            {
                return result;
            }
        }
        return null;
    } catch(Exception e) { log.error("findElement: "+e); return null; }
}
   

/**
 * Find scope (course/organization) selection from xml document
 * Note: This is customized for a spesific contract template!!
 * 
 * @param currentElement
 * @return selection
 */
private int findScopeSelection(Element currentElement)
{
    try {
        if (currentElement.getName().equals("RadioButton"))
        {
            log.debug("WordName: "+currentElement.getChildText("WordName"));
            if (currentElement.getChildText("WordName").equals("DrMSelectionRadioButton0")
                && currentElement.getChildText("DefaultCheck").equals("true"))
            {
                return 0;
            }
            else if (currentElement.getChildText("WordName").equals("DrMSelectionRadioButton1")
                && currentElement.getChildText("DefaultCheck").equals("true"))
            {
                return 1;
            }
        }

        Iterator itr = (currentElement.getChildren()).iterator();
        while(itr.hasNext()) {
            Element child = (Element)itr.next();
            int result = findScopeSelection(child);
            if(result != -1)
            {
                return result;
            }
        }
        return -1;
    } catch (Exception e) { log.error("findScopeSelection: "+e); return -1; }
}

Luokkaan fi.vy.vomyke.drm.ContractStateHandler pitää lisätä seuraava import-määritys:
import org.dspace.content.*;

Lisäksi changeContractState -funktioon tarvitaan muutamia lisäyksiä, jotka ovat näkyvissä alla olevassa listauksessa:
public void changeContractState(DRMContract cont, int contractState, Request request) throws SQLException // Lisätty
{
         
    // Lisatty
    DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
    if (!(dso instanceof org.dspace.content.Item))
        return;
    org.dspace.content.Item item = (org.dspace.content.Item) dso;
      
    try {
        if (contractState == DrmCoreServices.CONTRACT_STATE_VALID) cont.changeStateToValid(new Date(), item); // Lisätty parametri: item
        else if (contractState == DrmCoreServices.CONTRACT_STATE_WAITING) cont.changeStateToWaiting(item); // Lisätty: item
//   else if (contractState == CONTRACT_STATE_EXPIRED) cont.changeStateToExpired();
   else if (contractState == DrmCoreServices.CONTRACT_STATE_VOID) cont.changeStateToVoid(item); // Lisätty: item
   else if (contractState == DrmCoreServices.CONTRACT_STATE_DELETED) cont.markContractForDeletion(item); // Lisätty: item
    } catch (Exception e) { log.error(e); }
}

Luokan fi.vy.vomyke.drm.DRMContract funktioihin markContractForDeletion, changeStateToWaiting ja changeStateToVoid lisätään parametri Item item sekä lisätään seuraava rivi try -lohkojen sisään:
item.inheritCollectionDefaultPolicies(item.getOwningCollection());
Myös funktioon changeStateToValid lisätään parametri Item item sekä lisätään seuraava rivi try -lohkon sisään:
item.allowGroup();
Topic attachments
I Attachment Action Size Date Who Comment
Shib-dspace-principalname.patchpatch Shib-dspace-principalname.patch manage 7.6 K 29 Jul 2010 - 14:25 PasiHaekkinen Shib-dspace-principalname.patch
Print version |  PDF  | History: r6 < r5 < r4 < r3 | 
Topic revision: r6 - 29 Jul 2010 - 14:28:57 - PasiHaekkinen
 

TUTWiki

Copyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TUTWiki? Send feedback