Mamta Satoor wrote:

While going through some of the Derbylist mails on Grant/Revoke, I realized that various database objects in a sqlStandard mode may require switching authorizers from the invoker to different definers and vice versa. This piece of task sounds interesting to me and hence I have following proposal for implementing it.

I think it would be good to list what database objects you are looking to implement definer logic for. Derby currently uses invoker model for all database objects, like triggers, views and this new execution model in sqlAuthorization mode would be different. The Grant & Revoke spec also calls for droping some of these objects when privileges they need are revoked later.

Let's start with specific case of views, which in sqlStandard mode will always run with the definer authorization.
eg
say view v1 is defined by user "u1", and it does a select from a view v2 defined by user "u2".  And view v2 does a select from table t2, which is also owned by user "u2".
Now, when user "u1", after connecting to the database, runs select * from v1; the authorizers involved at different times during the execution of this sql will be different. First authorizer involved will always be for "u1", since "u1" is the user who has made connection to the database. This first authorizer is also the invoker authorizer. At the start of execution of select * from v1 by user "u1", since v1 is owned by "u1", the authorizer for "u1" should be used at that point. Next during the
execution of select * from v1, Derby is going to access v2 which is defined by user "u2" and hence, while accessing v2, the authorizer used should be corresponding to definer"u2". When the accessing of v2 is over, Derby should discard the authorizer for view definer "u2" and go back to authorizer for invoker "u1" to finish exeucting the rest of select * from v1. And at last, when execution of select * from v1 is over, the only user authorizer active should be the one for user "u1", who had made connection to the database for this session.

I am considering implementing above scenario of switching from one authorizer to another by keeping a stack of authorizers in GenericLanguageConnectionContext.

I don't think this model could work for views... Derby resolves views during binding by merging base view definition into the query that references the view. (For example: Select a from V1, with V1 being defined as SELECT a,b from t1 would become select a from (select a, b from t1)) This further gets resolved during preprocessing into something like select a from t1, by unnesting the subquery. (where possible) So by the time Derby gets into generation, there are no references to views, right?

Prior to intoduction of sqlStandard mode for Grant/Revoke, Derby was written to run everything with invoker as the authorizer. This single invoker authorizer information is currently kept in GenericLanguageConnectionContext's authorizer(GenericAuthorizer) field. This field gets set to invoker's authorizer when GenericLanguageConnectionContext gets initialized at the database connect time. This happens in GenericLanguageConnectionContext.initilize method. In addition to initilizing authorizer, this method also sets the default schema to a schema with the same name as the invoker. So, as we can see, GenericLanguageConnectionContext currently just deals with one authorizer and that authorizer is always the authorizer corresponding to the user who has made connection to the database.

But with the addition of Grant/Revoke to Derby, we can have different authorizers active during the execution of a single sql statement. To support multiple authorizers, GenericLanguageConnectionContext will need to keep a stack of these authorizers and a corresponding stack of default schemas descriptors for those authorizers. To achieve this, I am thinking of replacing "authorizer" field in GenericLanguageConnectionContext with Vector stackOfAuthorizers and Vector stackOfDefaultSchemasForAuthorizers. The initialize methd of GenericLanguageConnectionContext at the database connection time will create an authorizer for the user who made the connection to the database and this authorizer will be added as the first element to stackOfAuthorizers. This authorizer will stay on GenericLanguageConnectionContext's stack of authorizers until the user disconnects from the database. In addition, the default schema of this authorizer will be pushed on to the stack stackOfDefaultSchemasForAuthorizers.

This might work for triggers and constraints... other two database objects that need to implement definer execution model. But I have to think about it little more... to see if GenericLanguageConnectionContext is the right place for this. Don't triggers and constraints already have some soft of execution context that gets pushed or poped? Does it make sense to plug this mechanism into this? I have to research more on these questions..

It seems to me we need two different solutions... one for views and another for triggers/constraints. It is possible to extend what we may do for triggers/constraints later to routines, but views need to be handled differently. Much like synonyms, if we decided to extend definer model to synonyms. I don't know yet if synonyms are supposed to follow definer or invoker model.

Satheesh

In addition, two methods, addAuthorizerToTopOfStack and removeAuthorizerFromTopOfStack will be added to GenericLanguageConnectionContext and calls to these methods will be generated during the generation phase of a sql depending on what that sql's needs are for different authorizers. It will be the responsibility of the sql generation phase to generate calls to removeAuthorizerFromTopOfStack when it is finished with the authorizer that it added earlier using addAuthorizerToTopOfStack. The code changes for this new definer/invoker mode will also involve changing the signature of GenericLanguageConnectionContext's initialize method, such that the required authorizer name will be passed to this method and initialize method will create a GenericAuthorizer object using that name and will put that Authorizer on top of the authorizer stack. In addition, initialize method will also create default schema descriptor for that authorizer name and will put that on schema descriptor on top of the default schema descriptor stack. The GenericLanguageConnectionContext's changed initialize method will look something like this.
 public void initialize(String authorizerName, boolean sqlConnection) throws StandardException
 {
  //
  //Creating the authorizer authorizes the connection.
  Authorizer authorizer = new GenericAuthorizer(getAuthorizationId(authorizerName),this, sqlConnection);
  stackOfAuthorizers.addElement(authorizer);

  //we can ignore the following if this is a database connection
  //associated with internal thread such as logSniffer and StageTrunc
  if(!sqlConnection)
   return;

  /*
  ** Set the authorization id.  User shouldn't
  ** be null or else we are going to blow up trying
  ** to create a schema for this user.
  */
  if (SanityManager.DEBUG)
  {
   if (getAuthorizationId() == null)
   {
    SanityManager.THROWASSERT("User name is null," +
     " check the connection manager to make sure it is set" +
     " reasonably");
   }
  }


  SchemaDescriptor sd = initDefaultSchemaDescriptor();
  stackOfSessionsForAuthorizers.addElement(sd);
  setDefaultSchema(sd);  
 }

A call to this initialize method should be generated during the generation phase if an object requires a need to switch to the object definer's authorization. The code generation will involve passing the name of the object definer to GenericLanguageConnectionContext's initialize method and authorizer for the definer will be put
on GenericLanguageConnectionContext's stack of authorizers. If an object needs to run with the invoker's authorization, code should be generated to peek at the first authorizer on the stack of authorizers (the first authorizer will always correspond to invoker's authorizer) and that authorizer will need to be added to the top of authorizer stack by generating code addAuthorizerToTopOfStack. To peek at the first element of stack to get invoker's authorizer, I am planning on adding method
getAuthorizerForConnectedUser to GenericLanguageConnectionContext.

Irrespective of which authorizer was added (definer/invoker), the sql code generation phase should make sure that it generates the code to pop that authorizer from the stack when it is finished with that authorizer.

Example of code generation for adding an invoker authorizer
 //We want to put the INVOKER as the authorizer on LanguageConnectionContext's authorizer stack.
 //We can find the INVOKER authorizer by looking at first element of LanguageConnectionContext's
 //authorizer stack which will always be the INVOKER of the sql statement. Hence, we simply
 //generate the code to copy the first element of the LanguageConnectionContext's authorizer stack
 //to the top of the stack.
 acb.pushThisAsActivation(mb);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null,
  "getLanguageConnectionContext", ClassName.LanguageConnectionContext, 0);
 acb.pushThisAsActivation(mb);
 mb.callMethod (VMOpcode.INVOKEINTERFACE, null,
  "getLanguageConnectionContext", ClassName.LanguageConnectionContext, 0);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null,
  "getAuthorizerForConnectedUser", " org.apache.derby.iapi.sql.conn.Authorizer", 0);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null,
  "addAuthorizerToTopOfStack", "void", 1);

Example of code generation for adding a definer authorizer
 //We want to put the DEFINER authorizer by finding the owner name of the object and generating
 //code to call GenericLanguageConnectionContext.initialize method on that owner name.
 //GenericLanguageConnectionContext.initialize method will generate the authorizer for the passed
 //name and will add that authorizer to top of stack of GenericLanguageConnectionContext's authorizer stack.
 acb.pushThisAsActivation(mb);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null,
  "getLanguageConnectionContext", ClassName.LanguageConnectionContext, 0);
 String objectOwnerName = getDataDictionary().getSchemaDescriptor(
  objectSchemaUUID, getLanguageConnectionContext().getTransactionCompile()).getSchemaName();
 mb.push(objectOwnerName);
 mb.push(true);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null, "initialize", "void", 2);

Example of code generation for popping the authorizer that was added earlier and should not be used anymore
 acb.pushThisAsActivation(mb);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null,
  "getLanguageConnectionContext", ClassName.LanguageConnectionContext, 0);
 mb.callMethod(VMOpcode.INVOKEINTERFACE, null,
  "removeAuthorizerFromTopOfStack", "void", 0);

In case the sql being executed runs into an exception, as part of the cleanup work, we need to remove all the authorizers (except the first one on the stack since it belongs to the "invoker" ie the user who has made this connection to the database) and their corresponding default schema descriptors from GenericLanugaugeConnectionContext's stack. The additional code in GenericLanugaugeConnectionContext.cleanupOnError will be as follows
  /*
  ** In case of error, need to cleanup the intermediate authorizers
  ** and their corresponding default schema descriptors.
  ** Remove all the authorizers and default schema descriptors except
  ** for the first one on the stack because first authorizer corresponds
  ** to the user who has made this database connection
   */
  for (int i = stackOfAuthorizers.size() - 1; i >= 1; i--) {
   stackOfAuthorizers.removeElementAt(i);
   stackOfSessionsForAuthorizers.removeElementAt (i);
  }
 
 
This logic is pretty simple and straight forward but can be confusing at the same time. So, if anyonehas any questions, pass them on my way.
 
thanks,
Mamta

Reply via email to