More projects · 03 / 05Software design · Java

Secure Data Container in Java

A generic container with its own users and access control, where every item remembers who inserted it: one specification written before the code, and two implementations on completely different representations that behave the same way.

Sharing, live. Click to replay.
first owner:alicebobcarol
2implementations of the same interface
18operations specified, 9 required and 9 optional
3checked exceptions of its own
1label, the first owner, behind every access rule
0external libraries
01 / The problem

Data that knows its owner.

The assignment: a collection that behaves like a small data storage service. Many users keep their objects in it, so the container has to know who they are, keep everyone's data apart, and let each owner decide who else may see what they stored.

  • Identity. Users register with an ID and a password; IDs are unique, and every operation on data first checks the pair.
  • Isolation. A user sees only their own items and the ones others chose to share with them.
  • Control. Whoever inserts an item stays its first owner: if they delete it, every copy they shared disappears too; a recipient can drop a shared item only for themselves.
02 / The specification

A contract before the code.

The type is an interface, SecureDataContainer<E>, written before any implementation in the style of Liskov and Guttag: an overview of the abstract type, then for each of its 18 operations what it requires, what it modifies, its effects and what it throws. Domain errors are checked exceptions of its own (ExistingUserException, UserNotFoundException, DataNotFoundException); a null argument is a programming error and throws the unchecked NullPointerException.

SecureDataContainer.java · remove
E remove(String owner, String passw, E data) throws DataNotFoundException, UserNotFoundException;      /*      REQUIRES: owner != null && passw != null && data != null        THROWS: - NullPointerException if !(owner != null && passw != null)  (UNCHECKED)                                  or if data == null                - UserNotFoundException if user is not found in the container (CHECKED)                - DataNotFoundException if data is not found in the user owned items (CHECKED)      MODIFIES: this       EFFECTS: If user is the firstOwner the method will delete his data and all the same data                that he shared to others, otherwise (if user is not the firstOwner) it will only                delete his data     */
The specification of remove, from the interface: the first-owner rule is part of the contract, not an implementation detail.
03 / Two representations

Same abstraction, different data.

The two classes implement the interface on data structures that have little in common. The first keeps a map of users and one list with every item of every user, each labeled with who can access that copy and who inserted it. The second keeps one list per user, and an item only needs to remember its first owner. Here is the same state in both, after alice stores a report and shares it with bob:

Implementation 1 · a single list
users · HashMapalice → ●●●bob → ●●●
container · LinkedList<Item>Item(owner alice, first alice, report)Item(owner bob, first alice, report)
Implementation 2 · a list per user
users · ArrayList<User>User(alice, ●●●)Dato(first alice, report)User(bob, ●●●)Dato(first alice, report)

Abstract value, the same for both: alice → { report } · bob → { report }

Each class documents the bridge between the two levels: an abstraction function, which maps the concrete fields to the value the user sees (each user and the items they can access), and a representation invariant, the conditions every operation must preserve: no null fields, no null elements, no two users with the same ID.

First / SecondSecureDataContainer.java · fields
// Implementation 1: every item of every user in one listprivate class Item {    private String owner;       // who has access to this copy    private String firstOwner;  // who inserted the data    private E data;}private Map<String, String> users;   // user ID -> password (a HashMap)private List<Item> container;        // a LinkedList// Implementation 2: one list per userprivate class Dato<E> {    private String firstOwner;    private E data;}private class User<E> {    private String id;    private String passw;    ArrayList<Dato<E>> container;}private ArrayList<User<E>> users;
The fields of the two representations, condensed from the two classes (comments added; dato is Italian for "datum").
04 / Ownership rules

One label, every rule.

Every access rule follows from the first-owner label. The demo at the top runs exactly these operations:

put and share

Inserting makes the caller the first owner. Sharing gives another registered user a copy of the item, with the first owner unchanged.

remove

The first owner deletes the item and every copy shared with others; anyone else removes only their own access.

copy and get

A copy is still labeled with the original first owner. get works only on items the caller can access.

clear and removeUser

Both delete a user's items together with every copy they shared, so nothing outlives its owner's decision.

FirstSecureDataContainer.java · remove
if (userIsInTheContainer(owner, passw)) {    if (userOwnsTheData(owner, data)) {        int index = indexOfUserOwnsTheData(owner, data);        E copy = container.get(index).data;        if (container.get(index).firstOwner.equals(owner)) {            // I am the first owner            for (int i = 0; i < container.size(); i++) {                if (container.get(i).firstOwner.equals(owner)                        && container.get(i).data.equals(data)                        && !(container.get(i).owner.equals(owner))) {                    container.remove(i);                    i--; //to compensate the removal                }            }            int finalIndex = indexOfUserOwnsTheData(owner, data);            container.remove(finalIndex);        } else {            // I am not the first owner            container.remove(index);        }        return copy;    } else        throw new DataNotFoundException("Element has not been found!");} else    throw new UserNotFoundException("User not found in the container");
The body of remove in the first implementation: authentication, access check, then the first-owner rule (comments translated from Italian).
05 / Testing

One scenario, both implementations.

The test driver declares the container through the interface type, SecureDataContainer<Esame>, with an immutable exam record as test data (esame is Italian for exam), and contains the same scenario for each implementation, printing every user's view after each step:

  • three users register and each inserts two records;
  • records are shared, then removed by their first owner (every copy goes) and by a recipient (only that access goes);
  • a user copies a shared record, another clears all their data, and finally one deletes their account: the copies they had shared disappear with it.

Both implementations print the same data at every step: two very different representations, one behavior, exactly what the specification promises.

One contract, kept twice.

The interface, both implementations, the exceptions and the test scenario are on GitHub, with a README that explains the design. This was the Java half of the Programming II project; the OCaml half is the interpreter in OCaml.

Next project · 04 / 05Concurrent Supermarket Simulator in C