Ma

6

Hel

2012

18:34

Tuplatusina

Sauli Niinistö on tasavallan kahdestoista presidentti. Eilisessä presidentinvaalissa valittu Niinistö astuu tehtäväänsä maaliskuun alussa. Suomi astuu samalla uuteen, historialliseen aikakauteen, kun sekä pääministeri että presidentti toimivat Kokoomuksen mandaatilla. Päivää voi kutsua tuplatusinaksi, sillä kahdestoista presidentti tuli valittua sopivasti vuonna 2012. Pitää myöntää, että hieman vaalit jännittivät, kun haastaja Pekka Haaviston suosio alkoi nousta tasaisesti mitä lähemmäksi vaalipäivää tultiin. Kaikki oli siis periaatteessa mahdollista. Niinistön luvut olivat kuitenkin selvät ja ennakkosuosikin kansansuosio kesti loppuun asti. Toiselle kierrokselle selvinneet ehdokkaat olivat kummatkin tyylikkäitä ja sopivaa presidenttiainesta. Herrasmiesmäiseksi luonnehdittu Niinistö pysyi loppuun asti rauhallisena toisin kuin ensimmäisellä kierroksella lehdistön kanssa malttinsa menettänyt sosiaalidemokraattien ehdokas. Nytpä sitten jäämme katsomaan ja toteamaan, miten uuden presidentin kuusivuotinen valtakausi lähtee käyntiin.

Su

5

Hel

2012

18:31

Simple row level permissions - part 2

The first part of this article described how to set up the database tables to implement simple row level permissions. This part shows how to actually use the tables and query books with different access rights. To query access rights I use an extensive SQL query. The basic idea is to first find books that have some restrictions set and then list the rest of the books, which do not have any restrictions and are publicly available. For the following examples we add some permission into the table. Book title "Cooking for the weekend" is only available for users working for "Bestseller Cookhouse" company. John and Mary can see the "Miss Marble’s detective story" book while it is hidden for others. All other books are visible to all users.

bookId  companyId  userId
------  ---------  ------
4       100        NULL
3       NULL       10
3       NULL       11

Next I start building the query to check the rights. The first query finds individual users who have rights to certain books.

SELECT bookId, userId
  FROM Permissions
 WHERE userId IS NOT NULL;

This will return two rows as expected:

bookId  userId
------  ------
3       10
3       11

Following the requirements we need to check company rights, which inherit to the users working in those companies. The following query will return three rows.

SELECT p.bookId, m.userId
  FROM Permissions p, Members m
 WHERE p.companyId = m.companyId
   AND p.companyId IS NOT NULL;

All users (John, Mary and Evelyn) work for company ID 100 (Bestseller Cookhouse), which was listed in the Permissions table. Administrators and editors have always all rights to all books, so the following query returns six rows.

SELECT p.bookId, u.id as userId
  FROM Permissions p, Users u
 WHERE u.role IN ('admin', 'editor');

The Permissions table currently has two books, which have special access rights. One of these books is listed twice, because two users have personal rights to these items. Totaling three rows and one administrator and one editor makes a Cartesian product, which contains six rows. Next we need to combine all these queries with UNION and if using UNION DISTINCT of MySQL, we can get rid of the duplicate rows.

SELECT bookId, userId
  FROM Permissions
 WHERE userId IS NOT NULL
 UNION DISTINCT
SELECT p.bookId, m.userId
  FROM Permissions p, Members m
 WHERE m.companyId = p.companyId
   AND p.companyId IS NOT NULL
 UNION DISTINCT
SELECT p.bookId, u.id as userId
  FROM Permissions p, Users u
 WHERE u.role IN ('admin', 'editor');

The list of special access rights retrieved from the Permissions table looks like this:

bookId  userId
------  ------
3       10
3       11
3       14
3       15
4       10
4       11
4       13
4       14
4       15

The list is still missing the books, which have no access restrictions. All such books will have no user ID to check to, so I let the query return NULL value in place of userId column. I query only books, which are not listed in the Permissions table and use an inner query for simplicity in this example. The following SQL does the job:

SELECT id AS bookId, NULL as userId
  FROM Books
 WHERE id NOT IN (
SELECT DISTINCT bookId
  FROM Permissions
);

Combining this with the rest of the queries produces an SQL, which I wrap as a view:

CREATE OR REPLACE VIEW Rights AS
SELECT bookId, userId
  FROM Permissions
 WHERE userId IS NOT NULL
 UNION DISTINCT
SELECT p.bookId, m.userId
  FROM Permissions p, Members m
 WHERE m.companyId = p.companyId
   AND p.companyId IS NOT NULL
 UNION DISTINCT
SELECT p.bookId, u.id as userId
  FROM Permissions p, Users u
 WHERE u.role IN ('admin', 'editor')
 UNION DISTINCT
SELECT id AS bookId, NULL as userId
  FROM Books
 WHERE id NOT IN (
SELECT DISTINCT bookId
  FROM Permissions
);

In addition to the earlier list this query will add three more rows:

bookId  userId
------  ------
1       NULL
2       NULL
5       NULL

So, books with ID values 1, 2 and 5 are freely available with no restrictions. I like to work with simple structures and this is why I wrap the huge query as a database view, which is a lot easier to use in the application logic later.

Using the Rights view

When the user logs in the application, his or her user ID is stored in the session and used each time the system makes queries to the Books table. Let’s assume that Peter has logged in. His user ID is 12 and the query looks like this:

SELECT b.*
  FROM Books b, Rights r
 WHERE b.id = r.bookId
   AND (r.userId = 12
    OR r.userId IS NULL);

Peter sees a book listing:

id  title
--  ------------------------------
1   Summer holiday in Spain
2   Illustrated English dictionary
5   Sailing around the world

Peter has no personal access rights to special books nor does he work for any company that is listed in the Permissions table, and he is able to view only the publicly available books. Now Cheryl the editor logs in. Her user ID is 15 and the books she sees using the query above are listed below:

id  title
--  ------------------------------
1   Summer holiday in Spain
2   Illustrated English dictionary
3   Miss Marble’s detective story
4   Cooking for the weekend
5   Sailing around the world

Now that Cheryl has editor role and all editors have access to all books, she gets the whole list. Evelyn works for company ID 100 (Bestseller Cookhouse), which has access rights to book ID 4 (Cooking for the weekend). Evelyn gets a list of free books in addition to the book available to her company:

id  title
--  ------------------------------
1   Summer holiday in Spain
2   Illustrated English dictionary
4   Cooking for the weekend
5   Sailing around the world

She cannot see book "Miss Marble’s detective story", because it is explicitly granted to John and Mary only.

Final words

Setting up simple row level permissions requires an additional Permissions table and a view to check the access rights with each user. Heavy queries and views usually have their drawbacks as decreased performance. You can improve this by using indices and simple data structures.

La

4

Hel

2012

19:30

Simple row level permissions - part 1

Many applications store values in a database and usually grant various access rights to the stored data. Not all users have access to all information and even the access right levels vary. Setting up an access control system to certain entity is simple, if we only need to restrict some parts of the application or information. The application could, for example, allow some users to edit items while others can just view them. In case we need item (database row) level permissions, the programmer’s task becomes a bit trickier. Some users may edit only part of the items stored in the database and this requirement brings a whole new set of challenges.

Let’s assume we have a database with a product table. Typically the table would look something like this:

id  title
--  ------------------------------
1   Summer holiday in Spain
2   Illustrated English dictionary
3   Miss Marble’s detective story
4   Cooking for the weekend
5   Sailing around the world
CREATE TABLE Books (
  id INT NOT NULL,
  title VARCHAR(32) NOT NULL,
  PRIMARY KEY (id)
);

We also need a table for users. These users will be using the application and eventually have some access rights to the books table. Below is a simple user table.

id  name             role
--  ---------------  ------
10  John Doe	     user
11  Mary Jennings    user
12  Peter Moore      user
13  Evelyn Light     user
14  Mark Millington  admin
15  Cheryl Wood      editor
CREATE TABLE Users (
  id INT NOT NULL,
  name VARCHAR(32) NOT NULL,
  role VARCHAR(8) NOT NULL,
  PRIMARY KEY (id)
);

Without any access restrictions all users would have access to all books in the database. We now set a bunch of requirements to add some flavor to this example.

  • The application has three roles: users, editors and administrators.
  • Editors and administrators have all access rights to all books.
  • Editor or administrator may set the access rights to a user or a company the user works for.
  • Each user belonging in some company inherits the access rights granted to that company.
  • If no access rights are set, the book is publicly available to all users.
  • User may belong to multiple companies simultaneously.

According to the requirements we need a company table.

id   name
---  --------------------
100  Bestseller Cookhouse
101  Editorial Magazine
CREATE TABLE Companies (
  id INT NOT NULL,
  name VARCHAR(32) NOT NULL,
  PRIMARY KEY (id)
);

Each user belongs to at least one company. For this linking we need another table, for example Members, which is just a simply link table.

companyId  userId
---------  ------
100        10
100        11
101        11
101        12
100        13
CREATE TABLE Members (
  companyId INT NOT NULL,
  userId INT NOT NULL,
  FOREIGN KEY (companyId) REFERENCES Companies (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  FOREIGN KEY (userId) REFERENCES Users (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE
);

The Members table tells that user ID 10 (John Doe) works for company ID 100 (Bestseller Cookhouse). User 11 (Mary Jennings) works for two companies, namely 100 and 101. The Permissions table contains the access rights to books and it also tells, which company or user has been granted to the item.

CREATE TABLE Permissions (
  bookId INT NOT NULL,
  companyId INT DEFAULT NULL,
  userId INT DEFAULT NULL,
  FOREIGN KEY (bookId) REFERENCES Books (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  FOREIGN KEY (companyId) REFERENCES Companies (id)
    ON DELETE SET NULL
    ON UPDATE CASCADE,
  FOREIGN KEY (userId) REFERENCES Users (id)
    ON DELETE SET NULL
    ON UPDATE CASCADE
);

This table has columns for the book item as well as company and user. When some user is granted to access a book, his or her user ID is inserted into this table along with the book ID. If a company is granted an access right, then the company ID is inserted instead of the user ID. Note that only company ID or user ID is populated. When the user ID is set, then the company ID is set to NULL and vice versa.

We have now set up the database tables needed to implement simple row level permissions. The next step includes granting some access rights to users and companies and creating a database view to query the rights. I will walk through these tasks in the latter part of this article.