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.




