Security using Goal Expansion
Recently I had a problem at work where I had an existing set of data and needed to put security around it.
The end goal was to restrict access to certain documents based on the user and their permissions.
The issue is that there may be several places where the documents are accessed from and all of then need to be secured. Using Prolog, this can be done quite easily by using
goal_expansion/2 to create a wrapper that applies to all calls to a predicate.
Consider that there are documents in a system, with different security requirements. Here is a very small set of documents for this example.
% Document (id, title, author, filename). document( 1, 'Something the Government should not Know', ['Nick Freedom','Ava Openness'], 'something_the_government_should_not_know.pdf' ). document( 2, 'The Lives Of Celebrities', ['Evie Scandlemaker'], 'the_lives_of_celebrities.pdf' ). document( 3, 'Hidden Conspiracy Theories of the Roman Empire', ['Hidius Conspirius'], 'hidden_theories_of_the_roman_empire.pdf' ). document( 4, 'Top Secret Spy Circuits', ['Mr Black'], 'DsnEhxwxbViVzUIM.pdf' ).
Then there are facts about who can access which document.
user_document/2, simply maps a username to a document id or set of document ids.
user_document('Mr Black', 4). user_document('The Government', D) :- member(D, [2, 3]). user_document('Joe Public', D) :- member(D, [1, 2]).
At this point there is no way to restrict the access of the documents because any call to
document/4 will be allowed, and we don't have access to the current user either.
So to restrict it,
goal_expansion/2 will be used to replace any call to
with a check with the current user against the
The first thing to do is force a user to be set, this is done by setting the user in a global variable that will need to be set when calling. The
b_getval/2 call will only allow variables that have been set within the call that is currently being made. Security around users is a different matter not covered here.
So to access the documents a call to
b_setval(user, User) will need to be made to set the user.
To achieve this a test rule is sufficeint.
document_list(User, document(Id, Title, Authors, Filename)) :- b_setval(user, User), document(Id, Title, Authors, Filename).
Now to create the security wrapper. Goal expansion is a bit fickle but it is easy if a few rules are remembered.
- Any goals being expanded must be after the
goal_expansion/2rule in the source.
- If any goals are called within the
goal_expansion/2rules, they must be before the goal expansion in the source.
- If you want to call the predicate being expanded before it is manipulated, then that call must be before the
goal_expansion/2rule in the source.
That means that the following code must be placed before the
document/4 predicates or the goal expansion won't take place.
has_document_access(document(Id, Name, Authors, Filename)) :- b_getval(user, User), nonvar(User), user_document(User, Id), document(Id, Name, Authors, Filename). goal_expansion( document(Id, Name, Authors, Filename), has_document_access( document(Id, Name, Authors, Filename) )).
goal_expansion/2 works by checking every call that is compliled after the expansion rule is defined then if the first parameter matches, that call is replaced with the second parameter.
has_document_access/1 rule works as follows:
- Get the user value from the variable.
- Make sure that the user value is actually set to a value and not a variable, or all users will be checked against.
- Check that the user has access to the requested document (this works in reverse as well if the document is not set).
- Retrieve the document details!
Now when the
document/4 is called directly, the call fails.
?- document(Id, Title, Authors, Filename). false.
And when we use the test predicate with a user (say Joe Public), only the documents that can be accessed are unified.
?- document_list('Joe Public', D). D = document(1, 'Something the Government should not Know', ['Nick Freedom', 'Ava Openness'], 'something_the_government_should_not_know.pdf') ; D = document(2, 'The Lives Of Celebrities', ['Evie Scandlemaker'], 'the_lives_of_celebrities.pdf'). ?-
The question is, how do we know if the expansion worked or not? One easy way is to do a listing of a rule that calls the goal, eg:
?- listing(document_list/2). document_list(User, document(Id, Title, Authors, Filename)) :- b_setval(user, User), has_document_access(document(Id, Title, Authors, Filename)). true. ?-
As you can see the
document_list/2 test rule has changed and now calls
document/4. This transformation will take place if you run from the Prolog shell or if you use a meta call.
This example is a bit derived, but this kind of substitution is quite handy.
term_expansion/2 are designed as refactoring tools
when a some part of a program needs to be changed but is already used several times. However, they are key to certain libraries to make code efficient as well.