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:
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.
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
Figura 3: a página Users (Usuários)
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
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.
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"
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.
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:
formato Word, 512 KB