Fox Pro

Atualização de Segurança

Originalmente publicado na Universal Thread Magazine

http://www.UTMag.com/Portuguese

Oito anos atrás, pediram-me para apresentar um documento técnico sobre um tópico do FoxPro para ser publicado no MSDN. O único outro documento neste site era o artigo técnico de Alan Griver sobre Programação Orientada a Objetos. Queria apresentar algo que tivesse a mesma importância para qualquer usuário de FoxPro.
Decidi então criar um sistema de segurança. Gosto muito do FoxPro, mas ele carece totalmente de um sistema de controle de acesso ou de manipulação de dados em uma aplicação. O SQL tem todos os recursos de segurança incorporados (apesar de que, se este for o único motivo pelo qual você estiver usando o SQL Server, seu preço é um pouco salgado). Mas estamos falando do FoxPro, então isso não pode ser tão complicado assim.    

Quebrei a cabeça sobre qual seria a forma mais simples de apresentar o problema, e, com isso, descobrir uma solução. Esta acabou sendo a parte mais difícil do trabalho. Dei uma olhada no sistema de segurança Novell, mas havia algumas coisas como "o usuário pode criar arquivos", e "o usuário pode deletar arquivos" que não tem utilidade alguma em uma aplicação de banco de dados. Nunca autorizo meus usuários a criar tabelas.
O que meus usuários querem é ter o controle sobre quem poderá acessar determinadas telas, quem poderá imprimir relatórios, quem poderá visualizar campos simultâneos na tela, quem poderá editar ou cancelar registros - e a te mesmo, quem está autorizado a visualizar a página 3 de um frame de página em um formulário. É esse o tipo de controle que meus usuários procuram.
Escrevi a aplicação e a enviei ao MSDN. A pessoa que fez sua revisão ficou muito bem impressionada. "Isto está excelente", ele exclamou. Parecia que ele acreditava que eu deveria vender o sistema, em vez de distribuí-lo gratuitamente. Força do hábito.... Ao longo desses últimos anos, centenas de pessoas fizeram o download do sistema. Mas eu tinha certeza que poderia fazer algo mais, e uma recente carta de um leitor me incentivou a fazê-lo. Portanto, esta é uma atualização a um artigo já publicado anteriormente. Desde a primeira implementação, não há uma única linha de código igual ao sistema anterior, mas a idéia central permanece a mesma. É possível carregar este sistema, entrar com alguns dados e invocar uma função on-line em cada lugar em que se quer controlar qualquer tipo de acesso e o sistema poderá funcionar. Vamos torcer para dar certo, e continuar, que tal?

As estruturas das tabelas

Construí um sistema de duas camadas, baseado nos direitos de grupos e nos direitos de usuários. Os usuários fazem parte de grupos, e os grupos têm direitos, mas os direitos aos usuários individuais deverão se sobrepor aos direitos conferidos a um usuário associado a um grupo.
A tabela User inclui um campo GroupName, além dos tradicionais campos do UserID, da senha e do nome completo do usuário. O campo GroupName que construí tem a extensão de 20 caracteres. Embora isso possa sacrificar a criatividade de alguns desenvolvedores responsáveis pela estruturação dos componentes de namespaces .NET, essa extensão servirá para a maior parte dos objetivos. Decidi também denominar tudo o que temos que controlar de "Processos" uma vez que eles podem se referir a seleções de menus, formulários, funções dentro de formulários (por exemplo o evento de Clicar), páginas de um frame de uma página ou outros. Estes também têm a extensão de 20 caracteres. Veja abaixo os layouts da tabela:

Structure for table: C:\UTMAGAZINE\SECURITY\USERS.DBF 

Field    Name    Type    Width

1    USERID    Character    10

2    PASSWORD    Character    10

3    FULLNAME    Character    30

4    GROUPNAME    Character    20

Structure for table: C:\UTMAGAZINE\SECURITY\GROUPS.DBF 

Field    Field Name     Type    Width

1    GROUPNAME    Character    20

Structure for table: C:\UTMAGAZINE\SECURITY\PROCESSES.DBF 

Field    Field Name    Type    Width

1    PROCESS    Character    20

Isso feito, incluí mais duas tabelas. A primeira lista os processos e permissões de grupo e a segunda lista os processos e permissões de usuário. Ambas as tabelas têm uma coluna "True/False" denominada "State" (Estado) para armazenar a permissão de acesso. À medida que os registros vão sendo incorporados a essas tabelas, o software irá considerar .T. como padrão de Estado. Você poderá remover um processo da lista "selecionada", para recusar sua permissão de uso. Entretanto, você poderá querer especificar que determinado grupo não terá acesso a alguma coisa, apenas para fins de documentação, mesmo que a impossibilidade de entrada conduza ao mesmo fim.

Structure for table: C:\UTMAGAZINE\SECURITY\GRPPRJOIN.DBF 

Field    Field Name    Type    Width

1    GROUPNAME    Character    20

2    PROCESS    Character    20

3    STATE    Logica    1

Structure for table: C:\UTMAGAZINE\SECURITY\USRPRJOIN.DBF 

Field    Field Name    Type    Width

1    USERID    Character    10

2    PROCESS    Character    20

3    STATE    Logica    1

Para gerenciar essas cinco tabelas, construí um formulário único com um frame de página de cinco páginas. Usei SET EXCLUSIVE ON (Configurar como exclusivo em...) em um programa de testes, que é um pouco restritivo.
Contudo, a maneira de efetuar o PACK de tabelas que outros usuários possam eventualmente manter abertas por um ou dois segundos é um tema para outro artigo. Basta dizer que, pelo fato de os usuários apenas lerem essas 5 tabelas por um instante, não haverá problema com o compartilhamento de tabelas no momento da inicialização.
A aplicação de usuário requer apenas que o usuário faça o logon e construa um pequeno cursor de permissões, como veremos abaixo.
A primeira página gerencia os nomes do grupo de usuários:

Cc564876.visual_fox_24img_002(pt-br,MSDN.10).gif

Figura 1: A página de Grupos

A segunda tela gerencia os nomes do processo.

These can be names of screens, functions, whatever. They generally don't have to precisely match the names of things in your application. However, if you do name each of your forms, the code to test whether the user has access to a form or not can be as easy as adding 4 lines of code to your base form template. More about that later.

A propósito, esta segunda tela mostra o que ocorre quando se clica em Add (Adicionar).

A página Groups acima funciona da mesma forma. Para fazer qualquer inclusão no campo que estiver abaixo da lista, essa inclusão será adicionada à lista.

Cc564876.visual_fox_24img_003(pt-br,MSDN.10).gif

Figura 2: a página de Processos

A página Users (Usuários) é um pouco mais complicada.
Eu utilizei um Shape com uma margem invisível para cobrir os campos de entrada que têm ligação com as colunas da tabela Users.

A Figura 3 mostra a tela em seu estado normal, ao passo que a Figura 4 mostra o que acontece quando se dá um duplo clique em um dos nomes.

O Add (Adicionar) gera os mesmos campos, só que em branco

Cc564876.img_004(pt-br,MSDN.10).gif

Figura 3: a página Users (Usuários)

Cc564876.img_005(pt-br,MSDN.10).gif

Figura 4: a página Users durante um processo de edição

Atribuindo direitos aos processos

O processo de descobrir os direitos de um usuário requer duas etapas. Na primeira etapa, apresento o usuário com um formulário de logon, retornando o nome do grupo ao qual o usuário está associado. Seleciono, em seguida, todos os registros para o grupo de usuários a partir da tabela GRPPRJOIN, colocando-os em uma tabela à parte (por exemplo, a tabela X1). Em seguida eu devolvo todos os registros dos usuários da segunda tabela, denominada USRPRJOIN, que contém as "autorizações" do usuário dentro das atribuições de seu grupo, incluindo-os em uma segunda tabela de reserva (por exemplo, X2). Depois disso, eu apago todos os registros de X1 que também estiverem presentes em X2, anexando todos os registros, tanto de X1 como de X2 em um cursor denominado Access, que é exclusivo para cada sessão de usuário (por se tratar de um cursor). O Access só tem duas colunas: a de Process (Char 20) e de State (Logical 1). Finalmente, eu faço sua indexação na coluna Process.

Vejamos um exemplo: se um usuário chamado Les pertencer ao grupo de Accounting (Contabilidade), ao fazer o logon, a aplicação recupera seu UserID ("Les") e o nome de seu grupo "Accounting". A aplicação puxa então todos os registros de Accounting do GRPPRJOIN (a tabela Join dos Processos de Grupo), colocando-os em uma tabela de reserva denominada X1.

GrpPrJoin --> X1

Group    Process    State    Process    State

Accounting    Reports    .T.    Reports    .T.

Accounting    ViewMoneyFields    .T.    ViewMoneyFields    .T.

Accounting    Customer    .T.    Customer    .T.

Accounting    Backup/Restore    .F.    Backup/Restore    .F.

Engineering    Reports    .T.        

Engineering    Posting    .F.        

Engineering    View Salaries    .F.        

...X1 tem

X1 tem, agora, 4 registros:

Reports    .T.

ViewMoneyFields    .T.

Customer    .T.

Backup/Restore    .F.

Em seguida, o programa puxa quaisquer registros de "autorização" para o usuário Les (da Tabela Join dos Processos de Usuários) colocando-os em uma segunda tabela de reserva denominada X2:

UsrPrJoin --> X2

User    Process    State    Process    State

Les    Reports    .F.    Reports    .F.

Les    ViewMoneyFields    .F.    ViewMoneyFields    .F.

Marilyn    Reports    .T.        

Marilyn    Posting    .F.        

X2 contém agora 2 registros:

Reports    .F.

ViewMoneyFields    .F.

Isto feito, a aplicação deleta os registro de X1 que contenham processos que já constem na tabela X2:

X1:

Customer    .T.

Backup/Restore    .F.

Finalmente, o programa cria um cursor de duas colunas denominado Access, que comporta apenas as colunas Process e State, anexa os registros de X1 e X2, descarta X1 e X2 e indexa o cursor em UPPER(Process).

Access (cursor):

Process           State

----------------- -----
Customer           .T.

Backup/Restore     .F.

Reports            .F.

ViewMoneyFields    .F.

Este cursor é indexado em UPPER(Process), possibilitando uma forma rápida de descobrir o que o usuário poderá ou não fazer, a partir de sua associação ao grupo e de suas autorizações individuais.

O cursor Access é criado no programa MAIN:

X1: 
Listing 1: 
* Program-ID...: Main.PRG 
* Purpose......: MAIN 
program for TESTBED project 
SET TALK OFF 
SET CONFIRM ON 
SET 
SAFETY OFF 
SET MULTILOCKS ON 
SET EXCLUSIVE OFF 
CLEAR 
CLOSE 
TABLES 
WITH _Screen 
.AddObject ( "Title1", "Title", 0, 0 ) 
.AddObject ( "Title2", "Title", 3, 3 ) ENDWITH 
DO FORM Logon TO 
Result 
IF LEFT ( Result, 1 ) = [Y] 
oApp = CREATEOBJECT ( 
[AppObject] ) 
oApp.UserID = SUBSTR ( Result, 2, 10 ) oApp.GroupName = 
SUBSTR ( Result,12, 20 ) 
BuildAccessCursor() && Creates a cursor 
named Access 
DO MENU.MPR 
READ EVENTS 
ENDIF 
CLOSE 
ALL WITH _Screen 
.RemoveObject ( "Title2" ) 
.RemoveObject ( 
"Title1" ) 
ENDWITH 
SET SYSMENU TO DEFAULT 
DEFINE CLASS Title 
AS Label Visible = .T. Height = 100 Width = 1000FontName = [Times 
New Roman] FontSize = 36 Caption = [Security system testbed] 
BackStyle = 0 && Transparent ForeColor = RGB(225,225,225) 
Left = 25 
PROCEDURE Init 
LPARAMETERS Up, Left 
THIS.Top = 
_Screen.Height - 100 
IF NOT EMPTY ( Up ) 
THIS.ForeColor = RGB ( 255, 0, 
0 ) 
THIS.Top = THIS.Top - Up 
THIS.Left = THIS.Left - Left 
ENDIF 
ENDPROC 
ENDDEFINE 
DEFINE CLASS AppObject AS Custom 
LoggedOn = .F. 
UserID = [] 
GroupName = [] 
ENDDEFINE

No código do programa Main, o Logon "DO FORM" e o "TO Results" invocam um formulário modal que devolve (em seu evento UNLOAD) um string constituído quer por "Y", quer por "N", seguido de um UserID de 10 caracteres, além do GroupName de 20 caracteres, no caso de o logon ter sido bem sucedido. Veja a tela do Logon.

Figura 5: O formulário de Logon

Figura 5: O formulário de Logon

Veja o BuildAccessCursor:

Listing 2: 
 * Program-ID...: BuildAccessCursor.PRG 
 * Purpose......: Builds access cursor for group and user based on logon 
   Temp1 = [X] + RIGHT(SYS(3),4) 
 a=INKEY(.1)      && Might be necessary on a fast computer 
 Temp2 = [X] + RIGHT(SYS(3),4) 
 CREATE CURSOR Access ( Process Char(20), State L ) 
    Cmd = [SELECT * FROM GRPPRJOIN WHERE GroupName = '] ; 
      + oApp.GroupName + [' INTO Table ] + Temp1 
 &Cmd 
    Cmd = [SELECT * FROM USRPRJOIN WHERE UserID    = '] ; 
      + oApp.UserID    + [' INTO Table ] + Temp2 
 &Cmd 
   SELECT &Temp1 
 DELETE FROM &Temp1 WHERE Process IN ( SELECT Process FROM &Temp2 ) 
 SET DELETED OFF 
   SELECT Access 
 APPEND FROM &Temp1 FOR NOT DELETED() 
 APPEND FROM &Temp2 
   USE IN &Temp1 
 USE IN &Temp2 
 ERASE  &Temp1..DBF 
 ERASE  &Temp2..DBF 
   INDEX ON UPPER ( Process ) TAG Process 
   IF USED ( [GRPPRJOIN] ) 
    USE IN  GRPPRJOIN 
 ENDIF 
 IF USED ( [USRPRJOIN] ) 
    USE IN  USRPRJOIN 
 ENDIF

A função que diz ao programa quem acessou a aplicação, e com que finalidade, é denominada HasAccess. Ela usa um único parâmetro, que é um nome de processo. Ao invocar o HasAccess ("Customer"), ele poderá devolver um .T. ou um .F., de acordo com o que foi carregado no cursor do Access no momento do logon.
Se quisermos utilizar este recurso para negar acesso a certos grupos ou usuários, basta dar RETURN .F. no Init do formulário ou no evento Load, se o HasAccess (FormName) for devolvido. Se você for muito rigoroso na nomeação de seus formulários, você poderá incorporar este recurso ao Init do modelo de seu formulário para que ele se aplique a todos seus formulários (desde que cada nome de formulário esteja incluído na tabela de Processos. Entretanto, o software simplifica bastante a construção destas duas tabelas. Eu, particularmente, consegui, em apenas 3 minutos, incluir uma dezena de processos, e atribuir vários níveis de acesso a quatro grupos diferentes). Veja o código:

* Program-ID...: HasAccess.PRG 
 * Purpose......: Returns .T. or .F. from cursor Access 
 PARAMETERS pProcess 
 SaveAlias = ALIAS() 
 *  Note: CURSOR "Access" is created when user logs in 
 SELECT Access 
 PreviousExactSetting = SET("EXACT") 
 SET EXACT ON 
 SEEK UPPER ( pProcess )      && This is why we indexed on UPPER(Process) in cursor Access 
 ReturnValue = IIF( FOUND(), State, .F. ) 
 SET EXACT &PreviousExactSetting 
 IF NOT EMPTY ( SaveAlias ) 
    SELECT    ( SaveAlias ) 
 ENDIF 
 RETURN ReturnValue

Agora que já mostrei como são usadas as duas tabelas para criar um cursor Access único para uso em tempo de execução, posso mostrar a Figura 6, que demonstra de que forma os processos são transferidos de uma lista "Available" (Disponível) para a lista "Selected" (Selecionada). A caixa de seleção na base da tela poderá ser utilizada para configurar os direitos do processo.
Clicar com o botão direito em uma entrada Selecionada conduz ao mesmo resultado. As mudanças são salvas no momento em que elas são feitas na tela.

Cc564876.img_007(pt-br,MSDN.10).gif

Figura 6: a seleção e atribuição de direitos de processo

A página de direitos de usuário é idêntica, exceto que ela armazena as autorizações de direitos que pertencem a este usuário em particular.
Construí um modelo de testes para essa aplicação, incluindo uma tela Customers que tem um campo para o valor em dinheiro. Utilizei uma classe baseada em uma caixa de texto, denominada "MoneyField" para os campos de valores em dinheiro. No código do evento Init do modelo-base do formulário, incluí o seguinte código:

*Listing 4: Init code of my base form template:
IF NOT HasAccess ( THISFORM.Name )
   MESSAGEBOX( "Access denied", 64, "Uccess denied", 2000 )
   RETURN .F.
ENDIF
THISFORM.SetAll ( [Visible], HasAccess ( [ViewMoneyFields] ), [MoneyField] )

Se você entrar e desligar o acesso do User na tela Customer, você até poderá entrar:

Eu posso fechar o formulário com grande facilidade, alterar meus direitos de acesso para .T., para "Customers" e o programa autorizará minha entrada.

Mas, como minha tabela de direitos de usuário contém um registro com o processo denominado "MoneyField", e .F. para State, o campo que está baseado na classe MoneyField estará invisível quando a tela não aparecer.

Figura 8: é muito simples ocultar os "MoneyFields"

Figura 8: é muito simples ocultar os "MoneyFields"

Por fim, para controlar quais usuários poderão imprimir relatórios, basta acrescentar 'HasAccess("Reports")' na caixa de diálogo Skip For do menu Builder.

Cc564876.img_009(pt-br,MSDN.10).gif

Figura 9: a eliminação de opções do menu

Com apenas algumas alterações, este sistema também irá funcionar com o SQL Server ou com XML Web Services. Como pudemos constatar, este sistema faz uma série de coisas que o sistema de segurança do SQL Server não consegue fazer.

O restante do código

O restante do código da tela AccessManager e os menus do modelo de testes está no arquivo Zip. Se você tiver qualquer problema com o código, mande-me um email.

Até mais.

Faça o download do arquivo zip com o conteúdo aqui apresentado.

Les Pinter é membro da Agencia de oradores INETA e o Diretor Presidente da Pinter Consultoria de San Mateo, California. Ele publica um jornal bimestral para desenvolvedores com artigos sobre Visual FoxPro, VB.NET, ASP.NET e SQL. Ele é orador frequente em grupos de usuários e conferências nos Estados Unidos e no exterior, falando em inglês, português, espanhol, russo e francês. Les também é piloto de avião.

Faça o download deste documento:

Atualização de Segurança

downl.gif formato Word, 512 KB