Implementação de Controle de Acesso Baseado em RBAC em Python com Modelagem, Configuração e Testes
Introdução
Este artigo explora como implementei um sistema de controle de autorizações utilizando o formato Role-Based Access Control (RBAC
). Baseado em pesquisas realizadas em diversas fontes, incluindo artigos e vídeos do YouTube, cheguei a uma solução que, embora simples, é eficaz e pode ser facilmente adaptada e aprimorada conforme necessário.
O que é RBAC
?
O termo RBAC
vem de Role-Based Access Control
, que, como o próprio nome indica, é um Controle de Acesso (Access Control - AC) baseado em papéis, também conhecidos como roles
. Em termos simples, isso significa que os privilégios de um usuário são definidos de acordo com o papel que ele desempenha dentro de um sistema.
Um Controle de Acesso (Access Control - AC) é uma maneira eficaz de limitar o que os usuários podem ver e fazer em um ambiente específico, de acordo com suas permissões. Dependendo do contexto, essas restrições podem ser vitais para manter a segurança e a organização de um sistema.
Por exemplo, em um cenário de SaaS
(Software as a Service), o controle de acesso pode ser usado para restringir o acesso a informações sensíveis, limitar as visualizações de páginas a determinados usuários, ou definir quem pode executar certas funções. Essa abordagem garante que cada usuário tenha acesso apenas ao que é necessário para desempenhar suas tarefas, ajudando a prevenir erros e aumentar a segurança.
Em termos de software, essa prática também é conhecida como autorização
ou authorization
, e é uma parte fundamental do design de sistemas que precisam gerenciar múltiplos níveis de acesso.
Modelos
A maneira de modelar um sistema para utilizar o Role-Based Access Control (RBAC
) pode variar conforme o autor. Embora exista uma definição formal clara, conhecida como RBAC96
, que estabelece os princípios fundamentais desse modelo, é importante considerar adaptações conforme as necessidades do sistema.
Por exemplo, se o objetivo é criar um SaaS
com múltiplos clientes, cada um com seu próprio banco de dados, pode ser mais apropriado adotar uma variante do RBAC
, como o Organization-Based Access Control (OrBAC
). Nesse modelo é criado uma camada a mais, no qual as permissões e o acesso dos usuários são definidos e limitados de acordo com a organização ou empresa que oferece o serviço, permitindo uma maior flexibilidade e personalização em ambientes multi-tenant
.
Outro aspecto interessante do RBAC
é a criação de roles
e permissions
. Normalmente, o cliente que utiliza o serviço não tem controle sobre essas definições. O que se vê hoje é que o prestador de serviços que cria e gerencia os papéis, determinando quais permissões estão associadas a cada usuário.
Modelagem
Conceitos
Para a minha modelagem, criei um cenário em que desejava controlar o acesso de usuários a determinados recursos na interface. A visualização desses recursos dependeria do papel atribuído a cada usuário. Os papéis que defini foram os seguintes:
guest
- um usuário que pode visualizar todos os recursos, mas sem permissões adicionaisuser
- um usuário autenticado, com permissões para comprar e vender produtospremium
- um usuário com acesso a recomendações de produtos com descontoadmin
- um usuário com permissão para visualizar e gerenciar todo o sistema
Visualmente, isso seria representado da seguinte forma:
Usuário guest
Usuário user
Usuário premium
Usuário admin
Note que, em cada um desses papéis, as permissões dos papéis anteriores são mantidas, mas isso não é uma regra obrigatória; as permissões podem ser definidas de forma independente conforme necessário.
Base de Dados
Para gerenciar algumas dessas permissões, segui o conceito de OrBAC
(Organization-Based Access Control), que é uma extensão do RBAC
(Role-Based Access Control
), adicionando uma camada extra para a gestão de organizações.
Neste modelo, um usuário pode pertencer a várias organizações e, da mesma forma, uma organização pode ter vários usuários. Como o relacionamento é muitos-para-muitos (n-n
), utilizei uma entidade intermediária para conectar as entidades Users
e Organizations
.
Você pode estar se perguntando sobre as permissões e os papéis (roles
). Optei por não incluí-los na modelagem para simplificar. Em vez disso, essas permissões são gerenciadas diretamente no código e mapeadas conforme necessário.
Implementação
Implementei essa solução em Python
utilizando um Jupyter Notebook
para demonstrar como as permissões são gerenciadas. Você pode encontrar o código completo da implementação do modelo no seguinte link: Contruindo um Controle de Acesso via RBAC com Python.
Configuração do Modelo de Dados
Primeiramente, criei o banco de dados, apagando-o caso ele já existisse. Essa abordagem é útil no contexto de um notebook:
import os
import sqlite3
database = 'rbac.db'
if os.path.exists(database):
os.remove(database)
conn = sqlite3.connect(database)
Em seguida, criei a conexão e as tabelas principais: Users
, Organizations
e Memberships
:
cursor = conn.cursor()
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS organizations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cnpj TEXT NOT NULL UNIQUE
);
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS membership (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
organization_id INTEGER NOT NULL,
role TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (organization_id) REFERENCES organizations (id)
);
""")
Com isso, o banco de dados está configurado para a próxima etapa.
Implementação RBAC
Iniciei definindo as roles
e permissions
necessárias:
from typing import Literal, TypedDict
Role = Literal["guest", "user", "premium", "admin"]
Permission = Literal["view", "buy", "sell", "discounts", "manage"]
permissions = {
"guest": ["view"],
"user": ["view", "buy", "sell"],
"premium": ["view", "buy", "sell", "discounts"],
"admin": ["view", "buy", "sell", "discounts", "manage"]
}
Em seguida, criei classes de tipagem para representar os dados como dicionários baseados nas colunas das tabelas:
class UserInfo(TypedDict):
id: int
username: str
class OrganizationInfo(TypedDict):
id: int
name: str
cnpj: str
class MembershipInfo(TypedDict):
id: int
user_id: int
organization_id: int
role: Role
Também desenvolvi uma função auxiliar para hash
de senha:
import hashlib
def hash(password: str) -> str:
byte_input = password.encode()
hash_object = hashlib.sha256(byte_input)
hash_hex = hash_object.hexdigest()
return hash_hex
Criei funções auxiliares para a gestão de usuários, organizações e associações. Como o foco não é descrever essa implementação, eu vou pular os detalhes dela, mas fique a vontate para me perguntar se sentir alguma dúvida.
class UserNotFoundError(Exception):
def __init__(self, *args: object) -> None:
super('User not found').__init__(*args)
class OrganizationNotFoundError(Exception):
def __init__(self, *args: object) -> None:
super('Organization not found').__init__(*args)
def find_user_by_username(username: str) -> UserInfo | None:
user = cursor.execute("SELECT id, username FROM users WHERE username = ?", (username,)).fetchone()
if not user:
return None
return UserInfo(
id=user[0],
username=user[1]
)
def find_organization_by_cnpj(cnpj: str) -> OrganizationInfo | None:
organization = cursor.execute("SELECT id, name, cnpj FROM organizations WHERE cnpj = ?", (cnpj, )).fetchone()
if not organization:
return None
return OrganizationInfo(
id=organization[0],
name=organization[1],
cnpj=organization[2]
)
def create_user(username: str, password: str) -> UserInfo:
hashed_password = hash(password)
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, hashed_password))
conn.commit()
user = find_user_by_username(username)
if not user:
raise UserNotFoundError()
return user
def create_organization(name: str, cnpj: str) -> OrganizationInfo:
cursor.execute("INSERT INTO organizations (name, cnpj) VALUES (?, ?)", (name, cnpj))
conn.commit()
organization = find_organization_by_cnpj(cnpj)
if not organization:
raise OrganizationNotFoundError()
return organization
def associate_user_with_organization(user_id: int, organization_id: int, role: Role) -> None:
cursor.execute("INSERT INTO membership (user_id, organization_id, role) VALUES (?, ?, ?)", (user_id, organization_id, role))
conn.commit()
def get_all_users() -> UserInfo:
users = cursor.execute("SELECT id, username FROM users").fetchall()
return [UserInfo(id=user[0], username=user[1]) for user in users]
def get_all_organizations() -> OrganizationInfo:
organizations = cursor.execute("SELECT id, name, cnpj FROM organizations").fetchall()
return [OrganizationInfo(id=organization[0], name=organization[1], cnpj=organization[2]) for organization in organizations]
Agora é a criação usuários e empresas para gerar os relacionamentos.
company = create_organization('Company', '57232717000186')
matheus = create_user('matheus1714', '123')
lost = create_user('lost', '123')
lucas = create_user('lucas', '123')
marcia = create_user('marcia', '123')
associate_user_with_organization(matheus['id'], company['id'], 'admin')
associate_user_with_organization(lucas['id'], company['id'], 'guest')
associate_user_with_organization(lost['id'], company['id'], 'user')
associate_user_with_organization(marcia['id'], company['id'], 'premium')
Por fim montei essa função final para me dizer se um determinado usuário pode ou não fazer algo. Se esse código estivesse em uma API, caso o usuário tentasse acessar um recurso não permitido um retorno bom poderia ser o 404
, pois para aquele usuário aquele recursos não só não é autorizado como não existe.
def has_permission(user_id: int, organization_id: int, permission: Permission) -> bool:
user_role = cursor.execute("SELECT role FROM membership WHERE user_id = ? AND organization_id = ?", (user_id, organization_id)).fetchone()
if user_role is None:
return False
user_role = user_role[0]
return permission in permissions[user_role]
Finalmente, criei uma função para verificar se um usuário tem uma permissão específica e realizei alguns testes visuais:
cases = [
{
"user": lost,
"company": company,
"permission": "view",
},
{
"user": lost,
"company": company,
"permission": "buy",
},
{
"user": lost,
"company": company,
"permission": "manage",
},
{
"user": lost,
"company": company,
"permission": "no_exist",
}
]
def format_cnpj(cnpj: str) -> str:
return f'{cnpj[:2]}.{cnpj[2:5]}.{cnpj[5:8]}/{cnpj[8:12]}-{cnpj[12:]}'
for objs in cases:
user = objs['user']
company = objs['company']
permission = objs['permission']
question = '{username}, da organização {company_name} ({cnpj}), tem permissão de {permission}?'.format(
username=user['username'],
company_name=company['name'],
cnpj=format_cnpj(company['cnpj']),
permission=permission
)
asnwer = 'R: {asnwer}'.format(
asnwer='Sim' if has_permission(user['id'], company['id'], permission) else 'Não'
)
print(question)
print(asnwer)
print()
Os resultados foram:
lost, da organização Company (57.232.717/0001-86), tem permissão de view?
R: Sim
lost, da organização Company (57.232.717/0001-86), tem permissão de buy?
R: Sim
lost, da organização Company (57.232.717/0001-86), tem permissão de manage?
R: Não
lost, da organização Company (57.232.717/0001-86), tem permissão de no_exist?
R: Não
Como mostrado, uma implementação simples pode ser suficiente para criar um sistema de autorização baseado em RBAC
.
Melhorias
Uma possível melhoria para esse sistema seria adicionar tabelas adicionais para gerenciar permissões e papéis (rules
). Isso seria útil em cenários onde há um grande número de recursos e papéis a serem gerenciados, permitindo uma gestão mais granular e flexível. Contudo, não acho necessário para um sistema desse tamanho que montei.
Conclusão
A minha implementação do controle de acesso baseada em RBAC
, por mais que seja simples, parece ser eficaz para gerenciamento de permissões e roles
em sistemas. A solução criada em Python
oferece uma base sólida e simples para fazer i controle de acesso, permitindo expanção. A adição de funcionalidades como gerenciamento dinâmico de permissões e roles
pode aumentar ainda mais a flexibilidade e escalabilidade do sistema, proporcionando uma solução robusta para ambientes variados. Contudo, isso também traria mais complexidade ao sistema.
Referências
Excelente e super didática aprensentação do conteúdo. Fiquei com dúvidas quanto a implementação e desculpe minhas perguntas de iniciantes, mas ajuda-me a visualizar o fim do tunel.
- Apesar de ter usado o Jupyter, e talvez todo o código esteja em sequência, um forma baseada em arquivos seria dividí-los em quantos arquivos?
- A função de hash tem o papel somente de proteger a senha do usuário? A saída dela é um hash diferente para senhas diferentes?
Por enquanto é isso... ;)
Parabéns pelo conteúdo!!