#REST API

0 Seguidores · 56 Postagens

A transferência representacional de estado (REST) é um estilo de arquitetura de software que define um conjunto de restrições a ser usado para criar web services. Os web services que estão em conformidade com o estilo de arquitetura REST, chamados de RESTful Web Services (RWS), fornecem interoperabilidade entre sistemas de computador na Internet. Os RESTful web services permitem que os sistemas solicitantes acessem e manipulem representações textuais de recursos web usando um conjunto uniforme e predefinido de operações sem estado. Outros tipos de web services, como SOAP web services, expõem seus próprios conjuntos arbitrários de operações.

Saber mais.

Artigo Heloisa Paiva · Out. 23, 2025 1m read

Olá a todos,

Esta é uma dica rápida sobre como usar URLs em serviços REST API sem distinção entre maiúsculas e minúsculas.

Se você tem uma classe que estende de %CSP.REST e Ens.BusinessService para criar um serviço REST API, e você definiu seu WebApplication em minúsculas:

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
    <Route Url="/user" Method="POST" Call="User"/>
    <Route Url="/login" Method="POST" Call="Login"/>
</Routes>
}

Ele só aceitará URLs em minúsculas, por exemplo: http://myserver/myproduction/user

0
0 19
Artigo Larissa Prussak · Jun. 30, 2025 3m read

Olá, desenvolvedores!

Observando a avalanche de ferramentas para desenvolvedores movidas por IA e baseadas em vibe-coding que vêm surgindo quase todo mês, com recursos cada vez mais interessantes, eu fiquei me perguntando se seria possível aproveitá-las com o InterSystems IRIS. Pelo menos para construir um frontend. E a resposta é: sim! Pelo menos com a abordagem que eu segui.

Aqui está minha receita para construir a interface via prompting conectada ao backend IRIS:

  1. Tenha uma REST API no lado do IRIS, que siga uma especificação Open API (swagger).
  2. Gere a interface com alguma ferramenta de vibe-coding (por exemplo, Lovable) e aponte essa interface para o endpoint da REST API.
  3. Pronto!

Aqui está o resultado do meu próprio exercício — uma interface 100% criada via prompt conectada a uma REST API IRIS, que permite listar, criar, atualizar e excluir registros de uma classe persistente (Open Exchange,código do frontend, vídeo):

Qual é a receita em detalhes?

0
0 32
Artigo Heloisa Paiva · Abr. 30, 2025 17m read

Olá Comunidade,

Gostaria de apresentar meu último pacoteOpenAPI-Suite.Este é um conjunto de ferramentas para gerar código ObjectScript a partir deuma especificação OpenAPI versão 3.0..  IEm resumo, estes pacotes permitem:

  • Gerar classes de servidor. É bem parecido com o código gerado por ^%REST mas o valor adicionado é o suporte à versão 3.0.
  • Gerar classes de cliente HTTP.
  • Gerar classes de produção de cliente (business services, business operation, business process, Ens.Request, Ens.Response).
  • Uma interface web para gerar e baixar o código ou gerar e compilar diretamente no servidor.
  • Converter especificações das versões 1.x, 2.x para a versão 3.0.
0
0 43
Artigo Heloisa Paiva · Abr. 18, 2025 18m read

Image generated by OpenAI DALL·E

Sou um grande fã de ficção científica, mas embora eu esteja totalmente a bordo da nave Star Wars (desculpas aos meus colegas Trekkies!), sempre apreciei os episódios clássicos de Star Trek da minha infância. A tripulação diversificada da USS Enterprise, cada um dominando suas funções únicas, é uma metáfora perfeita para entender os agentes de IA e seu poder em projetos como o Facilis. Então, vamos embarcar em uma missão intergaláctica, utilizando a IA como a tripulação da nossa nave e  audaciosamente ir audaciosamente ir homem jamais esteve!  Esse conceito de trabalho em equipe é uma analogia maravilhosa para ilustrar como os agentes de IA funcionam e como os usamos em nosso projeto DC-Facilis. Então, vamos mergulhar e assumir o papel de um capitão de nave estelar, liderando uma tripulação de IA em territórios inexplorados!

Bem-vindo ao CrewAI!

Para gerenciar nossa tripulação de IA, usamos uma estrutura fantástica chamada CrewAI.É enxuta, extremamente rápida e opera como uma plataforma Python multiagente. Uma das razões pelas quais a amamos, além do fato de ter sido criada por outro brasileiro, é sua incrível flexibilidade e design baseado em funções.

from crewai import Agent, Task, Crew

the taken quote

Conheça os Planejadores

No Facilis, nossos agentes de IA são divididos em dois grupos. Vamos começar com o primeiro, que gosto de chamar de "Os Planejadores".

O Agente de Extração

O papel principal do Facilis é receber uma descrição em linguagem natural de um serviço REST e criar automaticamente toda a interoperabilidade necessária. Portanto, nosso primeiro membro da tripulação é o Agente de Extração. Este agente tem a tarefa de "extrair" as especificações da API a partir da descrição fornecida pelo usuário.

Aqui está o que o Agente de Extração procura:

  • Host (obrigatório)
  • Endpoint (obrigatório)
  • Params (opcional)
  • Port (se disponível)
  • modelo JSON (para POST/PUT/PATCH/DELETE)
  • Autenticação (se aplicável)
    def create_extraction_agent(self) -> Agent:
        return Agent(
            role='Extrator de Especificações de API',
            goal='Extrair especificações de API de descrições em linguagem natural',
            backstory=dedent("""
                        Você é especializado em interpretar descrições em linguagem natural
                        e extrair especificações de API estruturadas.
            """),
            allow_delegation=True,
            llm=self.llm
        )

    def extract_api_specs(self, descriptions: List[str]) -> Task:
        return Task(
            description=dedent(f"""
               Extraia as especificações de API das seguintes descrições:
                {json.dumps(descriptions, indent=2)}
                
               Para cada descrição, extraia:
                - Host (obrigatório)
                - Endpoint (obrigatório)
                - Params (opcional)
                - Port (se disponível)
                - modelo JSON (para POST/PUT/PATCH/DELETE)
                - Autenticação (se aplicável)
                
                Marque quaisquer campos obrigatórios ausentes como 'missing'.
                Retorne os resultados em formato JSON como um array de especificações.
            """),
            expected_output="""Um array JSON contendo as especificações de API extraídas com todos os campos obrigatórios e opcionais""",
            agent=self.extraction_agent
        )

O Agente de Validação

Próximo na fila, o Agente de Validação! Sua missão é garantir que as especificações de API coletadas pelo Agente de Extração estejam corretas e consistentes. Ele verifica:

  1. Formato de host válido
  2. Endpoint começando com '/'
  3. Métodos HTTP válidos (GET, POST, PUT, DELETE, PATCH)
  4. Número de porta válido (se fornecido)
  5. Presença de modelo JSON para métodos aplicáveis.

    def create_validation_agent(self) -> Agent:
        return Agent(
            role='API Validator',
            goal='Validar especificações de API quanto à correção e consistência.',
            backstory=dedent("""
                 Você é um especialista em validação de API, garantindo que todas as especificações
                 atendam aos padrões e formatos necessários.
            """),
            allow_delegation=False,
            llm=self.llm
        )

 def validate_api_spec(self, extracted_data: Dict) -> Task:
        return Task(
            description=dedent(f"""
                Valide a seguinte especificação de API:
                {json.dumps(extracted_data, indent=2)}
                
                Verifique:
                1. Formato de host válido
                2. Endpoint começando com '/'
                3. Métodos HTTP válidos  (GET, POST, PUT, DELETE, PATCH)
                4. Número de porta válido (se fornecido)
                5. Presença de modelo JSON para métodos aplicáveis.
                
               Retorne os resultados da validação em formato JSON.
            """),
            expected_output="""Um objeto JSON contendo os resultados da validação com quaisquer erros ou confirmação de validade""",
            agent=self.validation_agent
        )

O Agente de Interação

Avançando, conhecemos o Agente de Interação, nosso Especialista em Interação com o Usuário. Seu papel é obter quaisquer campos de especificação de API ausentes que foram marcados pelo Agente de Extração e validá-los com base nas descobertas do Agente de Validação. Eles interagem diretamente com os usuários para preencher quaisquer lacunas.

O Agente de Produção

Precisamos de duas informações cruciais para criar a interoperabilidade necessária: namespace e nome da produção. O Agente de Produção interage com os usuários para coletar essas informações, de forma muito semelhante ao Agente de Interação.

O Agente de Transformação de Documentação

Assim que as especificações estiverem prontas, é hora de convertê-las em documentação OpenAPI. O Agente de Transformação de Documentação, um especialista em OpenAPI, cuida disso.

    def create_transformation_agent(self) -> Agent:
        return Agent(
            role='Especialista em Transformação OpenAPI',
            goal='Converter especificações de API em documentação OpenAPI',
            backstory=dedent("""
                Você é um especialista em especificações e documentação OpenAPI.
                Seu papel é transformar detalhes de API validados em documentação
                OpenAPI 3.0 precisa e abrangente.
            """),
            allow_delegation=False,
            llm=self.llm
        )

    def transform_to_openapi(self, validated_endpoints: List[Dict], production_info: Dict) -> Task:
        return Task(
            description=dedent(f"""
                Transforme as seguintes especificações de API validadas em documentação OpenAPI 3.0:
                
                Informações de Produção:
                {json.dumps(production_info, indent=2)}
                
               Endpoints Validados:
                {json.dumps(validated_endpoints, indent=2)}
                
               Requisitos:
               1. Gerar especificação OpenAPI 3.0 completa
               2. Incluir schemas de requisição/resposta apropriados
               3. Documentar todos os parâmetros e corpos de requisição
               4. Incluir autenticação se especificado
               5. Garantir formatação de caminho apropriada
                
                Retorne a especificação OpenAPI nos formatos JSON e YAML.
            """),
            expected_output="""Um objeto JSON contendo a especificação OpenAPI 3.0 completa com todos os endpoints e schemas.""",
            agent=self.transformation_agent
        )

The Review Agent

Após a transformação, a documentação OpenAPI passa por uma revisão meticulosa para garantir conformidade e qualidade. O Agente de Revisão segue esta lista de verificação:

1.Conformidade OpenAPI 3.0

  • Especificação de versão correta
  • Elementos raiz obrigatórios
  • Validação da estrutura do schema
  1. Completude
  • Todos os endpoints documentados
  • Parâmetros totalmente especificados
  • Schemas de requisição/resposta definidos
  • Esquemas de segurança configurados corretamente
  1. Verificações de Qualidade
  • Convenções de nomenclatura consistentes
  • Descrições claras
  • Uso adequado de tipos de dados
  • Códigos de resposta significativos
  1. Melhores Práticas
  • Uso adequado de tags
  • Nomenclatura de parâmetros consistente
  • Definições de segurança apropriadas

Finalmente, se tudo parecer bom, o Agente de Revisão reporta um objeto JSON saudável com a seguinte estrutura:

{
 "is_valid": boolean,
 "approved_spec": object (a especificação OpenAPI revisada e possivelmente corrigida),
 "issues": [array de strings descrevendo quaisquer problemas encontrados],
 "recommendations": [array de sugestões de melhoria]
}

    def create_reviewer_agent(self) -> Agent:
        return Agent(
            role='Revisor de Documentação OpenAPI',
            goal='Garantir a conformidade e qualidade da documentação OpenAPI',
            backstory=dedent("""
                Você é a autoridade final em qualidade e conformidade de documentação OpenAPI.
               Com vasta experiência em especificações OpenAPI 3.0, você revisa meticulosamente
               a documentação para precisão, completude e adesão aos padrões.
            """),
            allow_delegation=True,
            llm=self.llm
        )


    def review_openapi_spec(self, openapi_spec: Dict) -> Task:
        return Task(
            description=dedent(f"""
                Revise a seguinte especificação OpenAPI para conformidade e qualidade:
                
                {json.dumps(openapi_spec, indent=2)}
                
                Lista de Verificação da Revisão::
                1. Conformidade OpenAPI 3.0
                - Verificar a especificação de versão correta
                - Verificar os elementos raiz obrigatórios
                - Validar a estrutura do schema
                
                2. Completude
                - Todos os endpoints devidamente documentados
                - Parâmetros totalmente especificados
                - Schemas de requisição/resposta definidos
                - Esquemas de segurança configurados corretamente
                
                3. Quality Checks
                - Consistent naming conventions
                - Clear descriptions
                - Proper use of data types
                - Meaningful response codes
                
                4. Best Practices
                - Proper tag usage
                - Consistent parameter naming
                - Appropriate security definitions
                
                Você deve retornar um objeto JSON com a seguinte estrutura:
                {{
                    "is_valid": boolean,
                    "approved_spec": object (a especificação OpenAPI revisada e possivelmente corrigida),
                    "issues": [array de strings descrevendo quaisquer problemas encontrados],
                    "recommendations": [array de sugestões de melhoria]
                }}
            """),
            expected_output=""" Um objeto JSON contendo: is_valid (boolean), approved_spec (object), issues (array), e recommendations (array)""",
            agent=self.reviewer_agent
        )

O Agente Iris

O último agente no grupo do planejador é o Agente Iris, que envia a documentação OpenAPI finalizada para o Iris.


    def create_iris_i14y_agent(self) -> Agent:
        return Agent(
            role='Especialista em Integração Iris I14y',
            goal='Integrar especificações de API com o serviço Iris I14y',
            backstory=dedent("""
                  Você é responsável por garantir uma integração suave entre o sistema de
                 documentação da API e o serviço Iris I14y. Você lida com a
                 comunicação com o Iris, valida as respostas e garante a integração
                 bem-sucedida das especificações da API.
            """),
            allow_delegation=False,
            llm=self.llm
        )

    def send_to_iris(self, openapi_spec: Dict, production_info: Dict, review_result: Dict) -> Task:
        return Task(
            description=dedent(f"""
               Enviar a especificação OpenAPI aprovada para o serviço Iris I14y:

                Informações de Produção:
                - Nome: {production_info['production_name']}
                - Namespace: {production_info['namespace']}
                - É Novo: {production_info.get('create_new', False)}

               Status da Revisão:
                - Aprovado: {review_result['is_valid']}
                
                Retornar o resultado da integração em formato JSON.
            """),
            expected_output="""Um objeto JSON contendo o resultado da integração com o serviço Iris I14y, incluindo o status de sucesso e os detalhes da resposta.""",
            agent=self.iris_i14y_agent
        )

class IrisI14yService:
    def __init__(self):
        self.logger = logging.getLogger('facilis.IrisI14yService')
        self.base_url = os.getenv("FACILIS_URL", "http://dc-facilis-iris-1:52773") 
        self.headers = {
            "Content-Type": "application/json"
        }
        self.timeout = int(os.getenv("IRIS_TIMEOUT", "504"))  # in milliseconds
        self.max_retries = int(os.getenv("IRIS_MAX_RETRIES", "3"))
        self.logger.info("IrisI14yService initialized")

    async def send_to_iris_async(self, payload: Dict) -> Dict:
        """
        Enviar carga útil para o endpoint de geração do Iris de forma assíncrona.
        """
        self.logger.info("Enviando carga útil para o endpoint de geração do Iris.")
        if isinstance(payload, str):
            try:
                json.loads(payload)  
            except json.JSONDecodeError:
                raise ValueError("Invalid JSON string provided")
        
        retry_count = 0
        last_error = None

        # Cria timeout para o aiohttp
        timeout = aiohttp.ClientTimeout(total=self.timeout / 1000)  # Converte ms para seconds

        while retry_count < self.max_retries:
            try:
                self.logger.info(f"Attempt {retry_count + 1}/{self.max_retries}: Enviando requisição para {self.base_url}/facilis/api/generate")
                
                async with aiohttp.ClientSession(timeout=timeout) as session:
                    async with session.post(
                        f"{self.base_url}/facilis/api/generate",
                        json=payload,
                        headers=self.headers
                    ) as response:
                        if response.status == 200:
                            return await response.json()
                        response.raise_for_status()

            except asyncio.TimeoutError as e:
                retry_count += 1
                last_error = e
                error_msg = f"Timeout occurred (attempt {retry_count}/{self.max_retries})"
                self.logger.warning(error_msg)
                
                if retry_count < self.max_retries:
                    wait_time = 2 ** (retry_count - 1)
                    self.logger.info(f"Waiting {wait_time} seconds before retry...")
                    await asyncio.sleep(wait_time)
                continue

            except aiohttp.ClientError as e:
                error_msg = f"Failed to send to Iris: {str(e)}"
                self.logger.error(error_msg)
                raise IrisIntegrationError(error_msg)

        error_msg = f"Failed to send to Iris after {self.max_retries} attempts due to timeout"
        self.logger.error(error_msg)
        raise IrisIntegrationError(error_msg, last_error)

Conheça os Geradores

Nosso segundo conjunto de agentes são os Geradores. Eles estão aqui para transformar as especificações OpenAPI em interoperabilidade InterSystems IRIS. Há oito deles neste grupo.

O primeiro deles é o Agente Analisador. Ele é como o planejador, traçando a rota. Seu trabalho é mergulhar nas especificações OpenAPI e descobrir quais componentes de Interoperabilidade IRIS são necessários.


    def create_analyzer_agent():
        return Agent(
            role="Analisador de Especificações OpenAPI",
            goal="Analisar minuciosamente as especificações OpenAPI e planejar os componentes de Interoperabilidade IRIS",
            backstory="""Você é um especialista em especificações OpenAPI e em Interoperabilidade InterSystems IRIS.
                     Seu trabalho é analisar documentos OpenAPI e criar um plano detalhado de como eles devem ser implementados como componentes de Interoperabilidade IRIS.""",
            verbose=False,
            allow_delegation=False,
            tools=[analyze_openapi_tool],
            llm=get_facilis_llm()
        )

   analysis_task = Task(
        description="""Analisar a especificação OpenAPI e planejar os componentes de Interoperabilidade IRIS necessários.
Incluir uma lista de todos os componentes que devem estar na classe de Produção."",
        agent=analyzer,
        expected_output="Uma análise detalhada da especificação OpenAPI e um plano para os componentes IRIS, incluindo a lista de componentes de Produção",
        input={
            "openapi_spec": openApiSpec,
            "production_name": "${production_name}" 
        }
    )

Em seguida, os Agentes de Business Services (BS) e Business Operations (BO) assumem o controle. Eles geram os Business Services e as Business Operations com base nos endpoints OpenAPI. Eles usam uma ferramenta útil chamada MessageClassTool para gerar as classes de mensagens perfeitas, garantindo a comunicação.


    def create_bs_generator_agent():
        return Agent(
            role="Gerador de Produção e Business Service IRIS",
            goal="Gerar classes de Produção e Business Service IRIS formatadas corretamente a partir de especificações OpenAPI",
            backstory="""Você é um desenvolvedor InterSystems IRIS experiente, especializado em Produções de Interoperabilidade.
                 Sua expertise reside na criação de Business Services e Produções capazes de receber e processar requisições de entrada com base emespecificações de API."",
            verbose=False,
            allow_delegation=True,
            tools=[generate_production_class_tool, generate_business_service_tool],
            llm=get_facilis_llm()
        )

    def create_bo_generator_agent():
        return Agent(
            role="Gerador de Business Operation IRIS",
            goal="Gerar classes de Business Operation IRIS formatadas corretamente a partir de especificações OpenAPI",
            backstory="""Você é um desenvolvedor InterSystems IRIS experiente, especializado em Produções de Interoperabilidade.
                 Sua expertise reside na criação de Business Operations capazes de enviar requisições para sistemas externos com base em especificações de API.""",
            verbose=False,
            allow_delegation=True,
            tools=[generate_business_operation_tool, generate_message_class_tool],
            llm=get_facilis_llm()
        )

    bs_generation_task = Task(
        description="Gerar classes de Business Service com base nos endpoints OpenAPI",
        agent=bs_generator,
        expected_output="Definições de classes de Business Service do IRIS",
        context=[analysis_task]
    )

    bo_generation_task = Task(
        description="Gerar classes de Business Operation com base nos endpoints OpenAPI",
        agent=bo_generator,
        expected_output="Definições de classes de Business Operation do IRIS",
        context=[analysis_task]
    )

    class GenerateMessageClassTool(BaseTool):
        name: str = "generate_message_class"
        description: str = "Gerar uma classe de Mensagem IRIS"
        input_schema: Type[BaseModel] = GenerateMessageClassToolInput

        def _run(self, message_name: str, schema_info: Union[str, Dict[str, Any]]) -> str:
            writer = IRISClassWriter()
            try:
                if isinstance(schema_info, str):
                    try:
                        schema_dict = json.loads(schema_info)
                    except json.JSONDecodeError:
                        return "Error: Invalid JSON format for schema info"
                else:
                    schema_dict = schema_info

                class_content = writer.write_message_class(message_name, schema_dict)
                # Armazenar a classe gerada.
                writer.generated_classes[f"MSG.{message_name}"] = class_content
                return class_content
            except Exception as e:
                return f"Error generating message class: {str(e)}"

Depois que BS e BO fazem o que têm que fazer, é a hora do Agente de Produção brilhar! Este agente junta tudo para criar um ambiente de produção coeso.

Depois que tudo estiver configurado, o próximo na linha é o Agente de Validação. Este entra em cena para uma verificação final, garantindo que cada classe Iris esteja ok.

Em seguida, temos o Agente de Exportação e o Agente de Coleção. O Agente de Exportação gera os arquivos .cls, enquanto o Agente de Coleção reúne todos os nomes de arquivos. Tudo é passado para o importador, que compila tudo no InterSystems Iris.


    def create_exporter_agent():
        return Agent(
            role="Exportador de Classes IRIS",
            goal="Exportar e validar definições de classes IRIS para arquivos .cls adequados",
            backstory="""Você é um especialista em implantação InterSystems IRIS. Seu trabalho é garantir que as definições de classes IRIS geradas sejam devidamente exportadas como arquivos .cls válidos que
                possam ser importados diretamente para um ambiente IRIS.""",
            verbose=False,
            allow_delegation=False,
            tools=[export_iris_classes_tool, validate_iris_classes_tool],
            llm=get_facilis_llm()
        )
        
    def create_collector_agent():
        return Agent(
            role="Coletor de Classes IRIS",
            goal="Coletar todos os arquivos de classes IRIS gerados em uma coleção JSON",
            backstory="""Você é um especialista em sistema de arquivos responsável por reunir e
                organizar os arquivos de classes IRIS gerados em uma coleção estruturada.""",
            verbose=False,
            allow_delegation=False,
            tools=[CollectGeneratedFilesTool()],
            llm=get_facilis_llm()
        )

    export_task = Task(
        description="Exportar todas as classes IRIS geradas como arquivos .cls válidos",
        agent=exporter,
        expected_output="Arquivos .cls IRIS válidos salvos no diretório de saída",
        context=[bs_generation_task, bo_generation_task],
        input={
            "output_dir": "/home/irisowner/dev/output/iris_classes"  # Optional
        }
    )

    collection_task = Task(
        description="Coletar todos os arquivos de classes IRIS gerados em uma coleção JSON",
        agent=collector,
        expected_output="Coleção JSON de todos os arquivos .cls gerados",
        context=[export_task, validate_task],
        input={
            "directory": "./output/iris_classes"
        }
    )

Limitações e Desafios

Nosso projeto começou como um experimento empolgante, onde meus colegas mosqueteiros e eu almejávamos criar uma ferramenta totalmente automatizada usando agentes. Foi uma jornada selvagem! Nosso foco principal estava em integrações de API REST. É sempre uma alegria receber uma tarefa com uma especificação OpenAPI para integrar; no entanto, sistemas legados podem ser uma história completamente diferente. Pensamos que automatizar essas tarefas poderia ser incrivelmente útil. Mas toda aventura tem suas reviravoltas: Um dos maiores desafios foi instruir a IA a converter OpenAPI para Interoperabilidade Iris. Começamos com o modelo openAI GPT3.5-turbo, que em testes iniciais se mostrou difícil com depuração e prevenção de interrupções. A mudança para o Anthropic Claude 3.7 Sonnet mostrou melhores resultados para o grupo Gerador, mas não tanto para os Planejadores... Isso nos levou a dividir nossas configurações de ambiente, usando diferentes provedores de LLM para flexibilidade. Usamos GPT3.5-turbo para planejamento e Claude sonnet para geração, uma ótima combinação! Essa combinação funcionou bem, mas encontramos problemas com alucinações. A mudança para o GT4o melhorou os resultados, mas ainda enfrentamos alucinações na criação de classes Iris e, às vezes, especificações OpenAPI desnecessárias, como o renomado exemplo Pet Store OpenAPI. Nos divertimos muito aprendendo ao longo do caminho, e estou super animado com o futuro incrível nesta área, com inúmeras possibilidades!

1
0 42
Artigo Heloisa Paiva · Mar. 7, 2025 7m read

Introdução

Uma API REST (Representational State Transfer ou Transferência de Estado Representacional) é uma interface que permite que diferentes aplicações se comuniquem entre si através do protocolo HTTP, usando operações padrão como GET, POST, PUT e DELETE. APIs REST são amplamente utilizadas no desenvolvimento de software para expor serviços acessíveis por outras aplicações, possibilitando a integração entre diferentes sistemas.

No entanto, para garantir que as APIs sejam fáceis de entender e usar, uma boa documentação é essencial. É aqui que o OpenAPI entra em cena.

0
0 43
Artigo Heloisa Paiva · Mar. 5, 2025 6m read

O que é JWT?

JWT (JSON Web Token) é um padrão aberto (RFC 7519) que oferece um método leve, compacto e auto-contido para transmitir informações de forma segura entre duas partes. É comumente usado em aplicações web para autenticação, autorização e troca de informações.

Um JWT é tipicamente composto por três partes:

1. Cabeçalho JOSE (JSON Object Signing and Encryption) 
2. Payload (Carga útil)
3. Assinatura

Essas partes são codificadas no formato Base64Url e concatenadas com pontos (.) separando-as.

Estrutura de um JWT

Cabeçalho

{ "alg": "HS256", "typ": "JWT"}

Payload

0
0 43
Artigo Julio Esquerdo · Fev. 18, 2025 34m read

API REST com Swagger no InterSystems IRIS

Olá,

O protocolo HTTP permite a obtenção de recursos, como documentos HTML. É a base de qualquer troca de dados na Web e um protocolo cliente-servidor, o que significa que as requisições são iniciadas pelo destinatário, geralmente um navegador da Web.

As API REST se beneficiam deste protocolo para trocar mensagens entre cliente e servidor. Isso torna as APIs REST rápidas, leves e flexíveis. As API REST utilizam os verbos HTTP GET, POST, PUT, DELETE e outros para indicar as ações que desejam realizar.

0
0 57
Artigo Heloisa Paiva · Fev. 17, 2025 6m read

Monitorar sua implantação do IRIS é crucial. Com a descontinuação do System Alert and Monitoring (SAM), uma solução moderna e escalável é necessária para obter insights em tempo real, detecção precoce de problemas e eficiência operacional. Este guia aborda a configuração do Prometheus e Grafana no Kubernetes para monitorar o InterSystems IRIS de forma eficaz.

Este guia pressupõe que você já tenha um cluster IRIS implantado usando o InterSystems Kubernetes Operator (IKO), que simplifica a implantação, integração e gerenciamento.

 

Por que Prometheus e Grafana?

0
0 64
Artigo Julio Esquerdo · Fev. 14, 2025 5m read

HTTP e HTTPS com API REST

Olá,

O protocolo HTTP permite a obtenção de recursos, como documentos HTML. É a base de qualquer troca de dados na Web e um protocolo cliente-servidor, o que significa que as requisições são iniciadas pelo destinatário, geralmente um navegador da Web.

As API REST se beneficiam deste protocolo para trocar mensagens entre cliente e servidor. Isso torna as APIs REST rápidas, leves e flexíveis. As API REST utilizam os verbos HTTP GET, POST, PUT, DELETE e outros para indicar as ações que desejam realizar.

0
0 74
Artigo Julio Esquerdo · Fev. 12, 2025 5m read

Utilizando Flask, API REST e IAM com o InterSystems IRIS

Parte 3 – IAM

O InterSystems API Manager (IAM) é um componente que permite monitorar, controlar e gerir o tráfego de APIs baseadas em HTTP. Ele também atua como uma API gateway entre aplicações e servidores InterSystems IRIS. 

O documento publicado em https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_apimgr traz as informações sobre o produto.

0
0 30
Artigo Julio Esquerdo · Fev. 12, 2025 6m read

Utilizando Flask, API REST e IAM com o InterSystems IRIS

Parte 2 – Aplicativo Flask

Flask é um microframework de desenvolvimento web escrito em Python. Ele é conhecido por ser simples, flexível e permitir o desenvolvimento rápido de aplicações. 

A instalação do Flask é muito simples. Depois de ter o python instalado corretamente no seu sistema operacional precisamos instalar a biblioteca flask com o comando pip. Para o consumo de API REST é aconselhado o uso da biblioteca requests. O link a seguir traz um guia para a instalação do flask: https://flask.palletsprojects.com/en/stable/installation/

0
0 25
Artigo Julio Esquerdo · Fev. 12, 2025 10m read

Utilizando Flask, API REST e IAM com o InterSystems IRIS

Parte 1 – API REST

Olá,

Neste artigo vamos ver a implementação de uma API REST para realizar a manutenção de um CRUD, utilizando o Flask e o IAM.

Nesta primeira parte do artigo vamos ver a construção e publicação da API REST no Iris.

Primeiro, vamos criar a nossa classe persistente para armazenar os dados. Para isso vamos para o Iris e criamos nossa classe:

Class ERP.Cliente Extends (%Persistent, %Populate, %XML.Adaptor)

{

Property nome As %String;

Property idade As %Integer;

}

0
0 33
Artigo Julio Esquerdo · Fev. 11, 2025 7m read

Utilizando o Gateway SQL com Python, Vector Search e Interoperabilidade no InterSystems Iris

Parte 2 – Python e Vector Search

Uma vez que temos acesso aos dados da nossa tabela externa podemos utilizar tudo que o Iris tem de excelente com estes dados. Vamos, por exemplo, ler os dados da nossa tabela externa e gerar uma regressão polinomial com eles.

Para mais informações sobre o uso do python com o Iris veja a documentação disponível em https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=AFL_epython

0
0 38
Artigo Julio Esquerdo · Fev. 11, 2025 4m read

Utilizando o Gateway SQL com Python, Vector Search e Interoperabilidade no InterSystems Iris

Parte 1 – Gateway SQL

Olá,

Neste artigo vamos ver o uso do Gateway SQL no Iris. O Gateway SQL permite que o Iris tenha acesso a tabelas de outros bancos (externos) via ODBC ou JDBC. Podemos acessar Tabelas ou Visões de diversos bancos, como Oracle, PostgreSQL, SQL Server, MySQL e outros.

Do ponto de vista de uso é como se a tabela estivesse local na nossa instância Iris, porém o armazenamento é realizado em um local externo.

0
0 57
Artigo Julio Esquerdo · Nov. 1, 2024 13m read

Projeto 8 – REST Outbound Adapter

Olá. Vamos montar nossa próxima integração utilizando o adaptador SOAP Inbound Adapter chamando um BP que chamará um BO que utilizará o REST Outbound Adapter.

O BO que vamos construir irá chamar o nosso BS que montamos no artigo https://pt.community.intersystems.com/post/desenvolvendo-integra%C3%A7%C3%B5es-com-o-intersystems-iris-aplica%C3%A7%C3%A3o-rest mas, importante, note que poderíamos chamar qualquer serviço interno no nosso integrador ou outra instância IRIS, ou externo em qualquer outro sistema com esse adaptador.

0
0 62
Artigo Heloisa Paiva · Out. 22, 2024 9m read

django_logo

Descrição

Este é um modelo para um aplicativo Django que pode ser implantado no IRIS como um aplicativo Web nativo.

Instalação

  1. Clone o repositório
  2. Crie um ambiente virtual
  3. Instale os requisitos
  4. Rode o arquivo docker-compose
git clone
cd iris-django-template
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
docker-compose up

Uso

A URL base é http://localhost:53795/django/.

Endpoints

  • /iris - Retorna um objeto JSON com as 10 principais classes presentes no namespace IRISAPP.
  • /interop - Um endpoint de ping para testar a estrutura de interoperabilidade do IRIS.
  • /api/posts -Um endpoint CRUD simples para um objeto Post.
  • /api/comments - Um endpoint CRUD simples para um objeto Comment.

Como desenvolver deste modelo

Veja o artigo de introdução ao WSGI: wsgi-introduction.

TL;DR: Você pode ativar ou desativar o sinalizador DEBUG no portal de segurança para que as alterações sejam refletidas no aplicativo à medida que você desenvolve.

Apresentação do código

A aplicação Django é estruturada como se segue:

  • app - Pasta do projeto Django
    • app - Pasta da aplicação Django para configuração
      • settings.py - Arquivo de definições Django
      • urls.py - Arquivo de configuração de URL Django para conectar as visualizações às URLs
      • wsgi.py - Arquivo do Django WSGI
      • asgi.py - Arquivo do Django AGI
    • community - Pasta da aplicação Django para o aplicativo da comunidade, com CRUD nos objetos de Post e Comment
      • models.py - Arquivo de modelos do Djando para os objetos Post e Comment
      • views.py - Arquivo de visualizações Django para cessar os objetos Post e Comment
      • serializers.py - Arquivo Django de serializadores para os objetos Post e Comentário. * admin.py - Arquivo Django de administração para adicionar CRUD à interface administrativa.
      • migrations - Pasta Django de migrações para construir o banco de dados.
      • fixtures - Pasta Django de fixtures com dados de demonstração
    • sqloniris - Pasta do aplicativo Django para o aplicativo SQL no IRIS.
      • views.py - Arquivo Django de views para consultar o namespace IRISAPP.
      • apps.py - Arquivo de configuração do aplicativo Django.
    • interop - Pasta do aplicativo Django para o aplicativo de interoperabilidade.
      • views.py - Arquivo Django de views para testar a estrutura de interoperabilidade.
      • apps.py - Arquivo de configuração do aplicativo Django.
    • manage.py - Arquivo de gerenciamento Django.

app/settings.py

Este arquivo contém as configurações Django para o aplicativo.

...

# Definição de aplicação

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'community',
    'sqloniris',
    'interop',
    'rest_framework'
]

...

REST_FRAMEWORK = {
    # Use as permissões padrão do Django `django.contrib.auth` ,
    # ou permita acesso de apenas leitura para usuários não autenticados
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 20
}

...

DATABASES = {
    "default": {
        "ENGINE": "django_iris",
        "EMBEDDED": True,
        "NAMESPACE": "IRISAPP",
        "USER":"SuperUser",
        "PASSWORD":"SYS",
    }
}

Algumas definições importantes para notar:

  • INSTALLED_APPS - Contém a lista de aplicativos instalados no projeto Django.
  • community - O aplicativo Django para as operações CRUD nos objetos Post e Comentário.
  • sqloniris - TO aplicativo Django para as operações SQL no IRIS.
  • interop - O aplicativo Django para as operações de interoperabilidade.
  • rest_framework - O framework Django REST para a API REST.
  • REST_FRAMEWORK - Contém as configurações para o framework Django REST.
    • DEFAULT_PERMISSION_CLASSES - Somente usuários autenticados podem realizar operações CRUD.
    • DEFAULT_PAGINATION_CLASS - A classe de paginação para a API REST.
  • DATABASES - Contém as configurações para a conexão com o banco de dados IRIS.
    • Aqui estamos usando o mecanismo django_iris para conectar ao banco de dados IRIS.

app/urls.py

Este arquivo contém a configuração de URL para o aplicativo Django.

from django.contrib import admin
from django.urls import path,include
from rest_framework import routers
from community.views import PostViewSet, CommentViewSet
from sqloniris.views import index
from interop.views import index as interop_index

router = routers.DefaultRouter()
router.register(r'posts', PostViewSet)
router.register(r'comments', CommentViewSet)


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    path('iris/', index),
    path('interop/', interop_index)
]
  • router - Contém o roteador padrão para a API REST.
  • routeer.register - Registra os viewsets Post e Comentário no roteador.
  • urlpatterns - Contém os padrões de URL para o aplicativo Django
    • /admin/ - A interface administrativa Django.
    • /api/ -A API REST para os objetos Post e Comentário.
    • /iris/ - O endpoint SQL no IRIS.
    • /interop/ - O endpoint de interoperabilidade.

app/wsgi.py

Este arquivo contém a configuração WSGI para o aplicativo Django.

Este é o arquivo que temos que fornecer ao IRIS para executar o aplicativo Django.

Na seção Security->Applications->Web Applications, temos que fornecer o caminho para este arquivo.

  • Application Name
    • app.wsgi
  • Callable Name
    • application
  • WSGI App directory
    • /irisdev/app/app

community/models.py

Este arquivo contém os modelos Django para os objetos Post e Comentário.

from django.db import models

# Create your models here.
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()

class Comment(models.Model):
    content = models.TextField()
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
  • Post - O modelo para o objeto Post.
    • title - O título do post.
    • content - O conteúdo do post.
  • Comment - O modelo para o objeto Comentário.
    • content - O conteúdo do comentário.
    • post - A chave estrangeira para o objeto Post.
    • related_name - O nome relacionado para os comentários.

community/seializers.py

Este arquivo contém os serializadores Django para os objetos Post e Comentário.

Usando o framework Django REST, podemos serializar os modelos Django em objetos JSON.

from rest_framework import serializers
from community.models import Post, Comment

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('id', 'title', 'content', 'comments')

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ('id', 'content', 'post')
  • PostSerializer -O serializador para o objeto Post.
  • CommentSerializer -O serializador para o objeto Comentário.
  • fields - Os campos a serem serializados.

community/views.py

Este arquivo contém as views Django para os objetos Post e Comentário.

Usando o framework Django REST, podemos criar operações CRUD para os modelos Django.

from django.shortcuts import render
from rest_framework import viewsets

# Import the Post and Comment models
from community.models import Post, Comment

# Import the Post and Comment serializers
from community.serializers import PostSerializer, CommentSerializer

# Create your views here.
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
  • PostViewSet - O viewset para o objeto Post.
  • CommentViewSet - O viewset para o objeto Comentário.
  • queryset - O queryset para o viewset.
  • serializer_class - A classe de serializador para o viewset.

sqloniris/views.py

Este arquivo contém as views Django para as operações SQL no IRIS.

from django.http import JsonResponse

import iris

def index(request):
    query = "SELECT top 10 * FROM %Dictionary.ClassDefinition"
    rs = iris.sql.exec(query)
    # Convert the result to a list of dictionaries
    result = []
    for row in rs:
        result.append(row)
    return JsonResponse(result, safe=False)
  • index - A view para a operação SQL no IRIS.
  • query - A consulta SQL a ser executada no banco de dados IRIS.
  • rs - O conjunto de resultados da consulta.
  • result - A lista de listas do conjunto de resultados.
  • JsonResponse - A resposta JSON para a view, safe é definido como False para permitir lista de listas.

interop/views.py

Este arquivo contém as views Django para as operações de interoperabilidade.

from django.http import HttpResponse

from grongier.pex import Director

bs = Director.create_python_business_service('BS')

def index(request):
    result = bs.on_process_input(request)
    return HttpResponse(result, safe=False)
  • bs - O objeto de serviço de negócios criado usando a classe Director .
  • index -A view para a operação de interoperabilidade.
  • result - A view para a operação de interoperabilidade.

Observação: não usamos JsonResponse para simplificar o código, podemos usá-lo se quisermos retornar um objeto JSON.

Solução de Problemas

Como executar o aplicativo Django em modo autônomo

Para executar o aplicativo Django em modo autônomo, podemos usar o seguinte comando:

cd /irisdev/app/app
python3 manage.py runserver 8001

Isso executará o aplicativo Django na porta padrão 8001.

Observação: você deve estar dentro do contêiner para executar este comando.

docker exec -it iris-django-template-iris-1 bash

Reiniciando o aplicativo no IRIS

Esteja no modo DEBUG, faça várias chamadas para o aplicativo e as alterações serão refletidas no aplicativo.

Como acessar o Portal de Gerenciamento do IRIS

Você pode acessar o Portal de Gerenciamento do IRIS acessando http://localhost:53795/csp/sys/UtilHome.csp.

Executar este template localmente

Para isso, você precisa ter o IRIS instalado em sua máquina.

Em seguida, você precisa criar um namespace chamado IRISAPP.

Instale os requisitos.

# Move to the app directory
cd /irisdev/app/app

# python manage.py flush --no-input
python3 manage.py migrate
# create superuser
export DJANGO_SUPERUSER_PASSWORD=SYS
python3 manage.py createsuperuser --no-input --username SuperUser --email admin@admin.fr

# load demo data
python3 manage.py loaddata community/fixtures/demo.json

# collect static files
python3 manage.py collectstatic --no-input --clear

# init iop
iop --init

# load production
iop -m /irisdev/app/app/interop/settings.py

# start production
iop --start Python.Production

Como servir arquivos estáticos

Para servir os arquivos estáticos no aplicativo Django, podemos usar o seguinte comando:

cd /irisdev/app
python3 manage.py collectstatic

Isso coletará os arquivos estáticos do aplicativo Django e os servirá no diretório /irisdev/app/static.

Para publicar os arquivos estáticos no IRIS, configure a seçãoSecurity->Applications->Web Applications.

web_applications

0
0 35
Artigo Heloisa Paiva · Out. 18, 2024 4m read

wsgi_logo

Contexto

A Interface de Gateway de Servidor Web (WSGI) é uma convenção de chamada simples para servidores web encaminharem solicitações para aplicativos web ou frameworks escritos na linguagem de programação Python. WSGI é um padrão Python descrito detalhadamente em PEP 3333.

🤔 Ok, ótima definição, e qual o objetivo com o iris?

O IRIS 2024.2+ possui um novo recurso que permite que você execute aplicativos WSGI diretamente no IRIS. Esse recurso é uma ótima maneira de integrar o IRIS com outros frameworks e bibliotecas Python.

Isso segue a tendência de primeira experiência com Python, onde você pode usar Python para interagir com o IRIS, e agora você também pode executar aplicativos Python diretamente no IRIS.

Como usasr

Para instanciar um aplicativo WSGI no IRIS, você precisa configurá-lo na seção Segurança->Aplicações->Aplicações Web do Portal de Gerenciamento IRIS.

Exemplo simples com Flask:

Arquivo chamado app.py no diretório /irisdev/app/community:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

Configuração UI

imagem

Nesta seção, você pode configurar a aplicação WSGI providenciando:

  • Nome da aplicação

  • Isso corresponde ao nome de arquivo da aplicação WSGI

  • ex: app.py, mas sem a extensão .py : app

  • Nome chamável

    • A função chamável que será chamada pelo servidor WSGO

    • ex: app corresponde à variável app no arquivo app.py

      • app = Flask(__name__)
  • Diretório de aplicação WSGI

    • O caminho onde o aplicativo WSGI está localizado
    • ex: /irisdev/app/community
  • Tipo de protocolo Python

    • Pode ser wsgi ou asgi
      • wsgi é o valor padrão e será usado neste exemplo
      • asgi é para aplicações assíncronas
        • nós suportamos asgi sincronicamente por hora com o adaptador a2wsgi
  • DEBUG *Se marcado, o aplicativo WSGI será executado no modo de depuração

    • Isso é útil para fins de desenvolvimento, pois quaisquer alterações no aplicativo WSGI serão recarregadas automaticamente

CPF Merge

Você também pode configurar o aplicativo WSGI usando CPF. Aqui está um exemplo de configuração

[Actions]
CreateApplication:Name=/flask,NameSpace=IRISAPP,WSGIAppLocation=/irisdev/app/community/,WSGIAppName=app,WSGICallable=app,Type=2,DispatchClass=%SYS.Python.WSGI,MatchRoles=:%ALL,WSGIDebug=0,WSGIType=0

Arquivos de Log

Os logs do aplicativo WSGI são armazenados no arquivo WSGI.log localizado no diretório mgr da instância.

Exemplos

Here are some examples of WSGI applications that you can run on IRIS, they aim to show how to run different Python frameworks on IRIS.

Basically, the use case will be the same for all the frameworks:

Endpoints

  • /iris - Retorna um objeto JSON com as 10 principais classes presentes no namespace IRISAPP.
  • /interop - Um endpoint de ping para testar o framework de interoperabilidade do IRIS.
  • /posts - Um endpoint CRUD simples para um objeto Post
  • /comments - Um endpoint CRUD simples para um objeto Comentário.

Modelo de Objeto

Objeto Post:

  • id
  • título
  • conteúdo

Objeto Comentário

  • id
  • post_id (chave estrangeira para Post)
  • conteúdo

Flask

Django

FastAPI

Limitações

  • O ASGI é suportado de forma síncrona por enquanto com o adaptador a2wsgi
  • aplicações tornado ( jupyter, streamlit, .. ) não são suportadas, pois não são compatíveis com WSGI.
0
0 58
Artigo Julio Esquerdo · Out. 14, 2024 4m read

Projeto 3 – Requisição REST

Vamos montar nossa próxima integração utilizando uma aplicação REST. Para tal vamos utilizar um BS que chamará o BP do nosso serviço demo (ver Primeira Integração). Vamos reaproveitar o serviço que então poderá ser chamado via SOAP ou REST. Teremos então dois BS que irão chamar o mesmo BP. A imagem abaixo ilustra essa arquitetura:

O primeiro passo vai ser criar um novo BS que não terá nenhum adaptador. Esse BS será chamado pela nossa aplicação REST que veremos mais a frente:

0
0 106
Artigo Heloisa Paiva · Out. 14, 2024 9m read

Este programa de demonstração é usado para mostrar como um perfil FHIR personalizado pode ser empregado para validar a conformidade dos dados. O guia de implementação FHIR personalizado foi desenvolvido com base na Versão R4 do FHIR, e, neste exemplo, implementa a extensão do recurso Organização para validar a conformidade dos dados.

Instalação

  1. Faça o Download do projeto via Git clone.
  2. Execute docker-compose up -d para criar e iniciar o contêiner, a execução inicial baixará as imagens de contêiner necessárias e a execução do script levará de 5 a 10 minutos (dependendo da máquina). O InterSystems IRIS for Health image será criado, então o servidor FHIR será instalado e a especificação FHIR personalizada será importada para que possa ser usada para validar os dados.
  3. Importe os arquivos de caso de teste de TestCases no Postman para ver como as várias restrições FHIR são validadas 4.Depois que o contêiner for iniciado, você poderá ver o conteúdo doCustom IG.

Estrutura do código

FHIRValidation
├─ ExampleIG                        
│  ├─ ig.ini
│  ├─ input
│  │  ├─ fsh
│  │  │  ├─ alias.fsh
│  │  │  ├─ codesystems.fsh
│  │  │  ├─ organization.fsh
│  │  │  └─ valuesets.fsh
│  │  └─ pagecontent
│  │     └─ index.md
│  └─ sushi-config.yaml
├─ README.md
├─ README_zh.md
├─ TestCases
│  └─ FHIR Profile-based Validation  testcases.postman_collection.json
├─ docker-compose.yml
└─ image-iris
   ├─ Dockerfile
   └─ src
      ├─ FullIG
      ├─ IGPackages
      │  ├─ hl7.fhir.uv.extensions.r4#5.1.0.tgz
      │  ├─ hl7.terminology.r4#6.0.2.tgz
      │  └─ package.tgz
      └─ init
         └─ init.sh

Examplo IG

Todos os arquivos neste subdiretório são códigos-fonte SUSHI da especificação FHIR personalizada.

Casos de teste

Este subdiretório contém scripts de caso de teste baseados na API REST do FHIR, que precisam ser importados para o Postman

image-iris

Este subdiretório contém os arquivos necessários para a imagem InterSystems IRIS for Health: └─ src ├─ FullIG Este diretório armazena o IG FHIR personalizado gerado porSUSHI ├─ IGPackages Este diretório contém os arquivos de pacote para IGs FHIR personalizados └─ ini tEste diretório contém os scripts de inicialização da imagem Docker IRIS

Introdução ao pacote FHIR

A organização HL7 recomenda o uso de guias de implementação (Guia de Implementação](https://build.fhir.org/ig/FHIR/ig-guidance/)) para explicar como utilizar a especificação FHIR. Além de instruções para leitura por desenvolvedores (por exemplo, HTML), os guias de implementação geralmente incluem artefatos legíveis por máquina e aplicáveis diretamente, que podem ser usados para direcionar tarefas como geração de código e validação de dados.

O Guia de Implementação FHIR usa a especificação Pacote NPM para gerenciar dependências. Todas as definições de estrutura (StructureDefinition), conjuntos de valores (ValueSet) e outros recursos cobertos pelo guia são agrupados em um único pacote que pode ser usado pelo servidor FHIR para ler a especificação, gerar código do cliente ou realizar verificações de qualidade de dados.

O guia de implementação gerado pela ferramenta SUSHI contém vários arquivos de pacote. Neste exemplo, image-iris/src/IGPackages/package.tgz é o pacote gerado, que pode ser diretamente importado pelo Servidor FHIR IRIS. É importante observar que, além dos pacotes de recursos centrais (por exemplo, hl7.fhir.r4.core),a especificação FHIR completa precisa referenciar pacotes de recursos adicionais, como terminologia, extensões e assim por diante.

A documentação atual do mecanismo de referência da especificação FHIR ainda não está completa. Por exemplo, com base na versão R4 da especificação FHIR, além de referenciar hl7.fhir.r4.core, ela também precisa referenciar [hl7.fhir.uv.extensions.r4#5.1.0](https://simplifier.net/packages/hl7.fhir.uv.extensions.r4/ 5.1.0) e hl7.terminology.r4#6.0.2. Essas referências estão documentadas naversão R5mas não declaradas na versão R4, então o desenvolvedor precisa adicioná-las durante o processo de desenvolvimento.

Nesse caso, esses pacotes foram baixados na pasta image-iris/src/IGPackages e serão carregados como dependências antes da personalização do guia de implementação FHIR.

Introdução à validação FHIR

Veja a seção Recursos de Validação da especificação FHIR. A especificação FHIR foi projetada com mecanismos de verificação de qualidade de dados para uma ampla variedade de elementos, incluindo estruturas de dados, bases de atributos, campos de valor, ligações de código, restrições etc. A organização HL7, na especificação FHIR, não define qual intensidade de controle de qualidade seguir, mas recomenda que o princípio detolerância seja aplicado aos dados FHIR.

Para repositórios FHIR que armazenam recursos FHIR, garantir a qualidade dos dados é um pré-requisito para tornar a indústria da saúde valiosa e garantir a segurança das práticas de saúde. Portanto, ao construir um esquema de compartilhamento e troca de dados baseado em repositórios FHIR, mesmo que dados que não atendam aos requisitos de qualidade tenham que ser salvos, deve-se calibrar o sistema para marcar as não-conformidades e promover atividades de governança de dados para salvaguardar a segurança da saúde e os interesses dos consumidores de dados.

ODos vários métodos de validação de dados indicados pela especificação FHIR, o Validador FHIR e as Operações FHIR fornecem a cobertura mais abrangente para validação de qualidade de dados.

Este exemplo usará a operação $validate fornecida pelo InterSystems IRIS for Health para validar dados FHIR que ainda não foram salvos via o parâmetro de perfil. Os usuários também podem modificar o caso de teste para construir um parâmetro HTTP POST para validar o recurso FHIR estoque.

Também deve ser observado que a operação $validate, se chamada corretamente, retornará o resultado da validação via Http 200 e, se houver alguma não-conformidade, uma mensagem de erro será encapsulada no recurso OperationOutcome retornado em vez de identificar o erro via o código Http.

Extensões ao FHIR R4

As seguintes extensões foram feitas ao recurso Organização com base no FHIR R4:

1. Modifique a força de ligação da linguagem

Altere a força de ligação do idioma principal da organização para obrigatório

2. Cardinalidade do campo ativo alterada de 0...1 para 1...1

Isso torna o status do campo ativo um campo obrigatório, com um e somente um elemento

3. Cardinalidade do campo Nome alterada de 0..1 para 1..1

O nome da organização se torna um campo obrigatório com um e somente um elemento. Para referência, hospitais na China podem ter mais de um nome além do nome do hospital se tiverem licenças como um Centro de Emergência, um Centro de Dor Torácica e assim por diante. No entanto, é observado que essas licenças geralmente identificam a capacidade dos serviços prestados pela instituição de saúde em vez do nome legal que ela tem no sistema de registro da organização, e o ciclo de vida de tais licenças não coincide com o ciclo de vida da própria instituição de saúde. Portanto, o nome obtido da licença é apropriadamente considerado como a capacidade de serviço da organização de saúde em vez do nome exclusivo da organização. No FHIR, o nome derivado da capacidade de serviço pode ser fornecido através do recurso HealthcareService, que pode ser mais apropriadamente usado para expressar o conceito acima estabelecendo um relacionamento de referência muitos-para-um com o recurso Organização

4.Aumento no tipo de organização de instituições médicas

De acordo com os tipos de organização do padrão nacional chinês GB/T 20091-2021, os CodeSystem organizationtype-code-system e ValueSet organizationtype-vs são adicionados respectivamente, e a extensão mdm-organizationTypeExtension é adicionada ao recurso Organização através de Extensão. A extensão mdm-organizationTypeExtension é adicionada ao recurso Organização para que o recurso possa ser usado para representar o tipo de organização que identifica os tipos de organização chineses. A extensão é implementada fatiando a Extensão com uma cardinalidade de 1..1 para que o recurso Organização de Saúde deva ter um elemento de tipo de organização.

5. Restrições nos números de identificação de organizações de saúde

O padrão base FHIR não inclui o tipo do código de crédito social unificado para organizações chinesas, por esse motivo o CodeSystem cs-identifierType-code-system é adicionado e o Identificador é fatiado de acordo com seu tipo, para que ele possa expressar o código de crédito social. E o formato do código de crédito social segue as seguintes restrições:

  1. identifier.use deve assumir o valor oficial, ou seja, uso oficial/oficial
  2. identifier.type DEVE seguir cs-identifierType-code-system, system DEVE ser o uri do codesystem e code DEVE ser “USCC”.
  3. identifier.value deve seguir a restrição personalizada uscc-length-18, o campo deve ter 18 dígitos de comprimento, dos quais os primeiros 17 dígitos devem ser numéricos e o último 1 dígito deve ser numérico ou alfabético

Lista de Casos de Teste

1. Sem perfil - Tudo OK

O perfil correspondente do recurso não é declarado, portanto, o Servidor FHIR não validará os valores dos atributos no recurso e retornará apenas Tudo OK.

2. Campo desconhecido

Um atributo não definido isNational foi adicionado ao recurso, portanto, o mecanismo de validação retornou um erro de elemento não reconhecido

3. Cardinalidade errada - menos

Neste IG, a cardinalidade do atributo de nome do recurso Organização foi modificada para 1..1, o que significa que deve haver e somente um nome de organização. O nome não é preenchido neste caso de teste e, portanto, a validação de dados falha. Além disso, pode-se observar que Identifier.type foi estendido para incluir o Código de Crédito Social Uniforme como um tipo de identificador, que não está incluído na especificação FHIR R4, mas a força da ligação de código para este campo é apenas EXEMPLO, o que não força restrições. Portanto, o mecanismo de validação retorna a informação de nível de valor do campo de código não-conformidade sem relatar um erro.

4.Força de ligação

Neste Guia de Implementação (IG), a força de ligação do código do atributo de idioma da organização foi alterada para "obrigatório". Isso significa que o valor do campo deve estar de acordo com o conjunto de valores http://hl7.org/fhir/ValueSet/languages. No caso de teste onde o campo recebe o valor "wrong language" (idioma incorreto), como esse valor não está presente no conjunto de valores obrigatório, a validação resultará em um erro crítico

5.Valor errado

Neste IG, o campo de valor para o tipo de organização vem de organizationtype-code-system, portanto, quando o valor de code no elemento de extensão do tipo mdm-organizationTypeExtension, que tem um valor de "999", que não está no campo de valor, resultará em um erro de nível de erro.

6. Invariante falha

Neste IG, o código de crédito social de uma organização deve seguir a restrição personalizada uscc-length-18 (o campo deve ter 18 dígitos de comprimento, onde os primeiros 17 dígitos devem ser numéricos e o último 1 dígito deve ser numérico ou alfabético), e, portanto, violar essa restrição quando o último dígito for o caractere “%” resultará em um erro.

7.Perfil falha

Um único perfil para uma definição de recurso contém várias restrições, portanto, todos os problemas que não satisfazem o perfil serão detectados durante a validação, como os seguintes problemas neste exemplo:

  1. código de idioma errado
  2. tipo de organização errado
  3. campo de nome ausente
0
0 33
Artigo Heloisa Paiva · Jul. 4, 2024 9m read

Integrar aplicações frontend de React com serviços backend como a base de dados IRIS através de APIs REST pode ser uma forma poderosa de contruir aplicações web robustas. No entanto, um obstáculo comum que os desenvolvedores costumam encontrar é o problema de Cross-Origin Resource Sharing (CORS), que pode impedir que o frontend acesse os recursos no backend devido a restrições de segurança impostas pelos navegadores web. Nesse artigo, exploraremos como abordar os problemas de CORS ao integrar aplicações web de React com serviços backend de IRIS.

Criando o esquema

Começamos definindo um esquema simples chamado Pacientes:

Class Prototype.DB.Patients Extends %Persistent [ DdlAllowed ] {

Property Name As %String;

Property Title As %String;

Property Gender As %String;

Property DOB As %String;

Property Ethnicity As %String;
}

Você pode inserir alguns dados de teste na tabela. Pessoalmente, acho o Mockaroo útil quando se trata de criar dados falsos. Ele permite fazer o download dos dados de teste como um arquivo .csv que pode ser importado diretamente no Portal de Administração.

Definição de serviços REST

Logo, definimos alguns serviços REST.

Class Prototype.DB.RESTServices Extends %CSP.REST {

Parameter CONTENTTYPE = "application/json";
    
XData UrlMap [ XMLNamespace = "http://www/intersystems.com/urlmap" ]
{
<Routes>
    <Route Url = "/patients" Method="Get" Call="GetPatients"/>
    <Route Url = "/patient/:id" Method="Post" Call="UpdatePatientName"/>
</Routes>
}

ClassMethod GetPatients() As %Status
{
    #Dim tStatus As %Status = $$$OK

    #Dim tSQL As %String = "SELECT * FROM Prototype_DB.Patients ORDER BY Name"

    #Dim tStatement As %SQL.Statement = ##class(%SQL.Statement).%New()
    
    Set tStatus = tStatement.%Prepare(tSQL)

    If ($$$ISERR(tStatus)) Return ..ReportHttpStatusCode(..#HTTP400BADREQUEST, tStatus)

    #Dim tResultSet As %SQL.StatementResult

    Set tResultSet = tStatement.%Execute()

    #Dim tPatients As %DynamicArray = []

    While (tResultSet.%Next()) {
        #Dim tPatient As %DynamicObject = {}
        Set tPatient.ID = tResultSet.ID
        Set tPatient.Name = tResultSet.Name
        Set tPatient.Title = tResultSet.Title
        Set tPatient.Gender = tResultSet.Gender
        Set tPatient.DOB = tResultSet.DOB
        Set tPatient.OrderedBy = tResultSet.OrderedBy
        Set tPatient.DateOfOrder = tResultSet.DateOfOrder
        Set tPatient.DateOfReport = tResultSet.DateOfReport
        Set tPatient.Ethnicity = tResultSet.Ethnicity
        Set tPatient.HN = tResultSet.HN
        Do tPatients.%Push(tPatient)
    }
    Do ##class(%JSON.Formatter).%New().Format(tPatients)
    Quit $$$OK
}

ClassMethod UpdatePatientName(pID As %Integer)
{
    #Dim tStatus As %Status = $$$OK
    #Dim tPatient As Prototype.DB.Patients = ##class(Prototype.DB.Patients).%OpenId(pID,, .tStatus)
    If ($$$ISERR(tStatus)) Return ..ReportHttpStatusCode(..#HTTP404NOTFOUND, tStatus)
    #Dim tJSONIn As %DynamicObject = ##class(%DynamicObject).%FromJSON(%request.Content)
    Set tPatient.Name = tJSONIn.Name
    Set tStatus = tPatient.%Save()
    If ($$$ISERR(tStatus)) Return ..ReportHttpStatusCode(..#HTTP400BADREQUEST, tStatus)
    #Dim tJSONOut As %DynamicObject = {}
    Set tJSONOut.message = "patient name updated successfully"
    Set tJSONOut.patient = ##class(%DynamicObject).%New()
    Set tJSONOut.patient.ID = $NUMBER(tPatient.%Id())
    Set tJSONOut.patient.Name = tPatient.Name
    Do ##class(%JSON.Formatter).%New().Format(tJSONOut)
    Quit $$$OK
}

}

Logo, seguimos para registrar a aplicação web no portal de administração.

  1. No Portal de Adminsitração, navegue a Administração do Sistema -> Segurança -> Aplicação - Aplicação Web -> Criar Nova Aplicação Web.
  2. Preencha o formulário confome mostrado a seguir image
  3. As APIs definidas em Prototype/DB/RESTServices.cls estarão disponíveis em http://localhost:52773/api/prototype/*
  4. Agora podemos verificar que as APIs estão disponíveis solicitando os endpoints usando o Postman. image

Criando o frontend

Eu utilizei o Next.js para criar um frontend simples. Next.js é uma framework popular de React que permite que os desenvolvedores construam aplicações React renderizados do lado do servidor (SSR) com facilidade.

O meu frontend é uma tabela simples que mostra os dados de pacientes armazenados no IRIS e oferece a funcionalidade para atualizar os nomes dos pacientes.

 const getPatientData = async () => {
    const username = '_system'
    const password = 'sys'
    try {
        const response: IPatient[] = await (await fetch("http://localhost:52773/api/prototype/patients", {
        method: "GET",
        headers: {
          "Authorization": 'Basic ' + base64.encode(username + ":" + password),
          "Content-Type": "application/json"
        },
      })).json()
      setPatientList(response);
    } catch (error) {
      console.log(error)
    }
  }

Parece que temos tudo pronto, mas se executarmos npm run dev diretamente, obtemos um erro de CORS :(

Resolvendo CORS

Um erro de CORS ocorre quando uma aplicação web tenta fazer uma solicitação a um recurso em domínio diferente e a política de CORS do servidor restringe o acesso desde a origem do cliente, resultando em que a solicitação seja bloqueada pelo navegador. Podemos resolver o problema de CORS tanto no frontend como no backend.

Estabelecer headers de resposta (o enfoque de backend)

Primeiro, adicionamos o parâmetro HandleCorsRequest à mesma classe de dispatch Prototype/DB/RESTServices.cls onde definimos os endpoints da API.

Parameter HandleCorsRequest = 1;

Logo, definimos o método OnPreDispatch dentro de sua classe dispatcher para estabelecer os headers de resposta.

   ClassMethod OnPreDispatch() As %Status
    {
        Do %response.SetHeader("Access-Control-Allow-Credentials","true")
        Do %response.SetHeader("Access-Control-Allow-Methods","GET, PUT, POST, DELETE, OPTIONS")
        Do %response.SetHeader("Access-Control-Max-Age","10000")
        Do %response.SetHeader("Access-Control-Allow-Headers","Content-Type, Authorization, Accept-Language, X-Requested-With")
        quit $$$OK
    } 

Uso do proxy Next.js (o enfoque frontend)

No seu arquivo next.config.mjs, adicione a função de reescrita (rewrite) da seguinte maneira:

 /** @type {import('next').NextConfig} */
        const nextConfig = {
            async rewrites() {
                return [
                    {
                        source: '/prototype/:path',
                        destination: 'http://localhost:52773/api/prototype/:path'
                    }
                ]
            }
        };

        export default nextConfig;

E atualize qualquer URL de fetch desde http://127.0.0.1:52773/api/prototype/:path a /prototype/:path.

O produto final

Para continuação deixo o código para a página frontend:

'use client'
import { NextPage } from "next"
import { useEffect, useState } from "react"
import { Table, Input, Button, Modal } from "antd";
import { EditOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import base64 from 'base-64';
import fetch from 'isomorphic-fetch'
const HomePage: NextPage = () => {
  const [patientList, setPatientList] = useState<IPatient[]>([]);
  const [isUpdateName, setIsUpdateName] = useState<boolean>(false);
  const [patientToUpdate, setPatientToUpdate] = useState<IPatient>()
  const [newName, setNewName] = useState<string>('')
  const getPatientData = async () => {
    const username = '_system'
    const password = 'sys'
    try {
        const response: IPatient[] = await (await fetch("http://localhost:52773/api/prototype/patients", {
        method: "GET",
        headers: {
          "Authorization": 'Basic ' + base64.encode(username + ":" + password),
          "Content-Type": "application/json"
        },
      })).json()
      setPatientList(response);
    } catch (error) {
      console.log(error)
    }
  }

  const updatePatientName = async () => {
     let headers = new Headers()
    const username = '_system'
    const password = 'sys'
    const ID = patientToUpdate?.ID
    try {
      headers.set("Authorization", "Basic " + base64.encode(username + ":" + password))
      const response: { message: string, patient: { ID: number, Name: string } } =
        await (await fetch(`http://127.0.0.1:52773/api/prototype/patient/${ID}`, {
        method: "POST",
          headers: headers,
          body: JSON.stringify({Name: newName})
      })).json()
      let patientIndex = patientList.findIndex((patient) => patient.ID == response.patient.ID)
      const newPatientList = patientList.slice()
      newPatientList[patientIndex] = {...patientList[patientIndex], Name: response.patient.Name}
      setPatientList(newPatientList);
      setPatientToUpdate(undefined);
      setNewName('')
      setIsUpdateName(false)
    } catch (error) {
      console.log(error)
    }
  }
  const columns: ColumnsType = [
    {
      title: 'ID',
      dataIndex: 'ID',
    },
    {
      title: "Title",
      dataIndex: "Title"
    },
    {
       title: 'Name',
      dataIndex: 'Name',
      render: (value, record, index) => {
        return (
          <div className="flex gap-3">
            <span>{value}</span>
            <span className="cursor-pointer" onClick={() => {
              setIsUpdateName(true)
              setPatientToUpdate(record)
            }}><EditOutlined /></span>
          </div>
        )
      }
    },
    {
      title: "Gender",
      dataIndex: 'Gender'
    },
    {
      title: "DOB",
      dataIndex: "DOB"
    },
    {
      title: "Ethnicity",
      dataIndex: "Ethnicity"
    },
    {
      title: 'HN',
      dataIndex: "HN"
    }
  ]

  useEffect(() => {
    getPatientData();
  }, [])
  return (
    <>
      <div className="min-h-screen">
        <Modal open={isUpdateName} footer={null} onCancel={() => {
          setIsUpdateName(false);
          setPatientToUpdate(undefined);
          setNewName('')
        }}>
          <div className="flex flex-col gap-5 pb-5">
            <div>
              <div className="text-2xl font-bold">Update name for patient {patientToUpdate?.ID} </div>
            </div>
            <div className="text-xl">Original Name: { patientToUpdate?.Name}</div>
            <div className="flex flex-row gap-2">
              <Input className="w-60" value={newName} onChange={(event) => setNewName(event.target.value)} />
              <Button type="primary" onClick={updatePatientName}>OK</Button>
              <Button onClick={() => {
                setIsUpdateName(false)
                setPatientToUpdate(undefined);
                setNewName('')
              }}>Cancel</Button>
            </div>
          </div>
        </Modal>
      <div className="flex justify-center py-10">
        <div className="h-full w-4/5">
          {patientList.length > 0 && <Table dataSource={patientList} columns={columns}/>}
        </div>
        </div>
      </div>
    </>
  )
} 

export default HomePage

Agora, quando visitar http://localhost:3000, isso é o que verá: image

Repositório Github para o projeto: https://github.com/xili44/iris-react-integration

Gostaria de agradecer a Bryan (@Bryan Hoon), Julian(@Julian Petrescu) e Martyn (@Martyn Lee), da oficina de Singapura, por seu apoio e experiência.

0
0 89
Artigo Danusa Calixto · Abr. 5, 2024 11m read

Eu estava tentando encontrar uma solução para conceder aos clientes acesso anônimo a determinados endpoints de API e também proteger outros endpoints na minha API REST. No entanto, ao definir um Web App, você só pode proteger o aplicativo inteiro, e não partes específicas.

Procurei respostas na comunidade, mas não encontrei nenhuma solução exata, exceto uma recomendação para criar dois Web Apps separados, um protegido e outro não. No entanto, na minha opinião, essa estratégia requer muito trabalho e cria um overhead de manutenção desnecessário. Prefiro desenvolver minhas APIs começando pela especificação e decidir nela quais endpoints devem permitir o acesso anônimo ou não.

Neste artigo, forneço dois exemplos: um para a Autenticação Básica e outro para o JWT, que é usado no contexto do OAuth 2.0. Se você notar alguma falha nestes exemplos, me avise e farei as correções necessárias.

Pré-requisitos

Primeiro, defina um Web App para sua API REST. Configure-o para o acesso não autenticado e especifique os privilégios necessários para o aplicativo. Especifique apenas as funções e os recursos necessários para o sucesso do uso da API.

Crie uma classe, por exemplo, "REST.Utils", onde você implementará os classmethods helper que verificam as credenciais.

Class REST.Utils  
{

}

Autenticação Básica

Se você quiser um endpoint seguro usando a Autenticação Básica, use o seguinte método para verificar se o nome de usuário e a senha informados no cabeçalho da Autorização HTTP têm os privilégios corretos para acessar o endpoint de API restrito.

/// Confira se os usuários têm as permissões necessárias.
/// - auth: o cabeçalho da Autorização.
/// - resource: o recurso para verificar as permissões.
/// - permissions: as permissões verificadas.
/// 
/// Exemplo:
/// > Do ##class(REST.Utils).CheckBasicCredentials(%request.GetCgiEnv("HTTP_AUTHORIZATION", ""), "RESOURCE", "U")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckBasicCredentials(auth As %String, resource As %String, permissions As %String) As %Status
{
  /// Analise a sanidade da entrada  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Confira se o cabeçalho da autorização começa com Basic  
  if ($FIND(auth, "Basic") > 0) {
    /// Retire a parte "Basic" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  /// Decodifique o nome de usuário e a senha codificados em base64  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth, ":", 1)
  Set password = $PIECE(auth, ":", 2)

  /// Tente fazer login como o usuário informado no cabeçalho da Autorização  
  Set tStatus = $SYSTEM.Security.Login(username, password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Confira se o usuário tem as permissões necessárias  
  Set tStatus = $SYSTEM.Security.Check(resource, permissions)

  /// Retorne o status. Se o usuário tiver as permissões necessárias, o status será $$$OK  
  Return tStatus
}

No endpoint que você quer proteger, chame o método "CheckBasicCredentials" e confira o valor retornado. Um valor de retorno "0" indica a falha na verificação. Nesses casos, retornamos "HTTP 401" de volta ao cliente.

O exemplo abaixo verifica se o usuário tem o recurso "SYSTEM_API" definido com privilégios "USE". Se não tiver, retorne "HTTP 401" ao cliente. Lembre-se de que o usuário da API precisa ter o privilégio "%Service_Login:USE" para usar o método "Security.Login".

Exemplo

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckBasicCredentials(authHeader, "SYSTEM_API", "U")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... resto do código

JWT

Em vez de usar a Autenticação Básica para proteger um endpoint, prefiro usar os Tokens de Acesso JWT do OAuth 2.0, já que são mais seguros e oferecem uma maneira mais flexível de definir privilégios por escopos. O seguinte método verifica se o token de acesso JWT fornecido no cabeçalho da Autorização HTTP tem os privilégios corretos para acessar o endpoint de API restrito.

/// Verifique se o JWT fornecido é válido.
/// - auth: o cabeçalho da Autorização.
/// - scopes: os escopos que esse token JWT deve ter.
/// - oauthClient: o cliente OAuth que é usado para validar o token JWT. (opcional)
/// - jwks: o JWKS usado para a validação da assinatura do token (opcional)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Analise a sanidade da entrada  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }
  
  /// Confira se o cabeçalho da autorização começa com Bearer. Se sim, faça a limpeza do token.  
  if ($FIND(token, "Bearer") > 0) {
    /// Retire a parte "Bearer" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Crie uma lista da string de escopos  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Remova os espaços em branco de cada escopo  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decodifique o token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Obtenha o epoch time atual
  Set now = $ZDATETIME($h,-2)

  /// Confira se o token expirou  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Confira se o token tem os escopos necessários
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// Se o token é válido em todo o escopo e não expirou, confira se a assinatura é valida
  if (oauthClient '= "") {
    /// Se especificamos um cliente OAuth, use isso para validar a assinatura do token
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// Se especificamos um JWKS, use isso para validar a assinatura do token
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}

/// Decodifique um token JWT.
/// - token: o token JWT a decodificar.
/// - payload: o payload do token JWT. (Saída)
/// - header: o cabeçalho do token JWT. (Saída)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decodifique e processe o Cabeçalho  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decodifique e processe o Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}

Novamente, no endpoint que você quer proteger, chame o método "CheckJWTCredentials" e confira o valor retornado. Um valor de retorno "0" indica a falha na verificação. Nesses casos, retornamos "HTTP 401" de volta ao cliente.

O exemplo abaixo verifica se o token tem os escopos "scope1" e "scope2" definidos. Se não tiver os escopos necessários, houver expirado ou falhar na validação da assinatura, ele retornará um código de status "HTTP 401" ao cliente.

Exemplo

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckJWTCredentials(authHeader, "scope1,scope2")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... resto do código

Conclusão

Aqui está o código completo para a classe "REST.Utils". Se você tiver quaisquer sugestões sobre como melhorar o código, me informe. Atualizarei o artigo devidamente.

Uma melhoria óbvia seria verificar a assinatura JWT para garantir que é válida. Para fazer isso, você precisa ter a chave pública do emissor.

Class REST.Utils
{
  
/// Confira se os usuários têm as permissões necessárias.
/// - auth: o conteúdo do cabeçalho da Autorização.
/// - resource: o recurso para verificar as permissões.
/// - permissions: as permissões verificadas.
/// 
/// Exemplo:
/// > Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckBasicCredentials(authHeader, "RESOURCE", "U"))
/// 
/// Retorne: %Status. O status da verificação.  
ClassMethod CheckBasicCredentials(authHeader As %String, resource As %String, permissions As %String) As %Status
{
  Set auth = authHeader

  /// Analise a sanidade da entrada  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Confira se o cabeçalho da autorização começa com Basic  
  if ($FIND(auth, "Basic") > 0) {
    // Retire a parte "Basic" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  Try {
  /// Decodifique o nome de usuário e a senha codificados em base64  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth,":",1)
  Set password = $PIECE(auth,":",2)
  } Catch {
    Return $$$ERROR($$$GeneralError, "Not a valid Basic Authorization header")
  }

  /// Tente fazer login como o usuário informado no cabeçalho da Autorização  
  Set tStatus = $SYSTEM.Security.Login(username,password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Confira se o usuário tem as permissões necessárias  
  Set tStatus = $SYSTEM.Security.Check(resource, permissions)

  /// Retorne o status. Se o usuário tiver as permissões necessárias, o status será $$$OK  
  Return tStatus
}

/// Verifique se o JWT fornecido é válido.
/// - auth: o cabeçalho da Autorização.
/// - scopes: os escopos que esse token JWT deve ter.
/// - oauthClient: o cliente OAuth que é usado para validar o token JWT. (opcional)
/// - jwks: o JWKS usado para a validação da assinatura do token (opcional)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Analise a sanidade da entrada  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }
  
  /// Confira se o cabeçalho da autorização começa com Bearer. Se sim, faça a limpeza do token.  
  if ($FIND(token, "Bearer") > 0) {
    /// Retire a parte "Bearer" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Crie uma lista da string de escopos  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Remova os espaços em branco de cada escopo  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decodifique o token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Obtenha o epoch time atual
  Set now = $ZDATETIME($h,-2)

  /// Confira se o token expirou  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Confira se o token tem os escopos necessários
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// Se o token é válido em todo o escopo e não expirou, confira se a assinatura é valida
  if (oauthClient '= "") {
    /// Se especificamos um cliente OAuth, use isso para validar a assinatura do token
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// Se especificamos um JWKS, use isso para validar a assinatura do token
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}


/// Decodifique um token JWT.
/// - token: o token JWT a decodificar.
/// - payload: o payload do token JWT. (Saída)
/// - header: o cabeçalho do token JWT. (Saída)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decodifique e processe o Cabeçalho  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decodifique e processe o Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}
}
0
1 81
Artigo Danusa Calixto · Abr. 5, 2024 6m read

Neste artigo, vou abordar o teste e a depuração dos aplicativos da Web Caché (principalmente REST) com ferramentas externas. A segunda parte abrange ferramentas de Caché.

Você escreveu um código do servidor e quer testar em um cliente ou já tem um aplicativo da Web e ele não funciona. É aqui que entra a depuração. Neste artigo, vou mostrar desde as ferramentas mais fáceis de usar (navegador) até as mais completas (analisador de pacotes). Porém, primeiro, vamos falar um pouco sobre os erros mais comuns e como eles podem ser resolvidos.

Erros

401 Não Autorizado

Acho que é o erro encontrado com mais frequência ao implantar para a produção. O servidor de desenvolvimento local geralmente tem uma configuração de segurança mínima ou normal, mas convencional. No entanto, o servidor de produção pode ter um esquema mais restritivo. Portanto:

  • Confira se você fez login
  • Confira se o usuário tem acesso ao banco de dados/tabela/procedimento/linha/coluna que você quer acessar
  • Confira se a solicitação OPTIONS pode ser realizada por um usuário não autorizado

404 Não Encontrado

Confira:

  • Se o url está correto
  • Caso seja um aplicativo novo e você esteja usando um servidor da Web externo, recarregar o servidor da Web pode ajudar

Erros do aplicativo

De certa forma, é o mais fácil de encontrar — o stack trace ajuda. A resolução é completamente específica ao aplicativo.

Ferramentas de depuração

Navegador da Web

A principal ferramenta de depuração sempre disponível é o navegador da Web, de preferência o Chrome, mas o Firefox também é suficiente. As solicitações GET podem ser testadas ao inserir o URL na barra de endereço, todas as outras solicitações exigem um aplicativo da Web ou a escrita de código js. A abordagem geral é:

  • Pressione F12 para abrir as ferramentas de desenvolvedor.
  • Acesse a guia Network (Rede).
  • Marque a caixa de seleção "Preserve Log" (Preservar registro), se não estiver selecionada.
  • Exiba apenas solicitações XHR.
  • Realize a ação com problemas no aplicativo da Web.

Depois, você pode examinar as solicitações e reenviá-las. O Firefox também pode editar as solicitações antes de repeti-las.

Vantagens:

  • Sempre disponível
  • Fácil de usar (os usuários finais podem enviar capturas de tela das guias Network e Console)
  • É um ambiente de usuário final

Desvantagens:

  • Não mostra respostas parcialmente enviadas/corrompidas/etc.
  • Lento para respostas grandes
  • Lento para um número grande de respostas
  • Tudo é feito manualmente

Cliente REST

O cliente REST é um aplicativo da Web independente ou um complemento do navegador da Web criado especificamente para testar aplicativos da Web. Eu uso Postman, mas há muitos outros. Veja como é a depuração no Postman:

O Postman trabalha com solicitações agrupadas em coleções. A solicitação pode ser enviada no ambiente. O ambiente é uma coleção de variáveis. Por exemplo, no meu ambiente CACHE@localhost, a variável do host está definida como localhost e o usuário como _SYSTEM. Quando a solicitação é enviada, as variáveis são substituídas pelos seus valores para o ambiente escolhido e a solicitação é enviada.

Confira um exemplo de coleção e ambiente para o projeto MDX2JSON.

Vantagens:

  • Escreva uma única vez e use em todos os lugares
  • Melhor controle sobre a solicitação
  • Embelezamento das respostas

Desvantagens:

  • <s>A depuração de solicitações em cadeia (a resposta à request1 pode forçar a request2 ou request 2B) ainda é manual</s> (Atualização de 22 de fevereiro: possível no Postman)
  • Às vezes, falha com respostas parcialmente enviadas/corrompidas/etc.

Proxy de depuração HTTP

Um aplicativo independente que registra tráfego HTTP(S). As solicitações registradas podem ser modificadas e reenviadas. Eu uso Charles e Fiddler.

Vantagens:

  • Processa respostas parcialmente enviadas/corrompidas/etc.
  • Embelezamento das respostas
  • Melhor suporte para tráfego HTTPS (do que no analisador de pacotes)
  • Consegue capturar sessões

Desvantagens:

  • Algo (aplicativo da Web/cliente REST/código JS) é necessário para enviar a solicitação

Analisador de pacotes

Um programa de computador consegue interceptar e registrar o tráfego transmitido por uma rede. Com os fluxos de dados fluindo em toda a rede, o sniffer captura cada pacote e, se necessário, decodifica os dados brutos do pacote. Essa é a opção mais completa, mas também exige um pouco de habilidade para a operação adequada. Eu uso o WireShark. Veja um breve guia sobre a instalação e o uso:

  1. Se você quiser capturar pacotes locais, leia sobre o loopback e instale o software de pré-requisito (npcap para windows)
  2. Instale o WireShark
  3. Configure filtros de captura (por exemplo, um filtro para só capturar tráfego na porta 57772: port 57772
  4. Inicie a captura
  5. Configure filtros de exibição (por exemplo, um filtro para só exibir tráfego http para um ip específico: ip.addr == 1.2.3.4 && http

Confira um exemplo de captura de tráfego http (filtro de exibição) na porta 57772 (filtro de captura):

 Vantagens:

  • Processa respostas parcialmente enviadas/corrompidas/etc.
  • Consegue capturar uma grande quantidade de tráfego
  • Consegue capturar qualquer coisa
  • Consegue capturar sessões

Desvantagens:

  • Algo (aplicativo da Web/cliente REST/código JS) é necessário para enviar a solicitação

O que usar

Bem, isso depende da finalidade. Primeiro, podemos ter como objetivo registrar (proxy de depuração, analisador de pacotes) ou gerar (navegador, cliente REST) solicitações.

Se você estiver desenvolvendo uma API Web REST, o cliente REST é a maneira mais rápida de testar o funcionamento.

No entanto, se as solicitações do cliente REST funcionarem, mas o aplicativo da Web cliente não, talvez seja necessário o proxy de depuração http e o analisador de pacotes.

Se você tiver clientes e precisar desenvolver uma API do lado do servidor para trabalhar com eles, precisará do proxy de depuração http ou do analisador de pacotes.

É melhor estar familiarizado com todos os 4 tipos de ferramentas e mudar rapidamente entre eles se o atual não der conta do trabalho.

Às vezes, a ferramenta ideal é óbvia.

Por exemplo, recentemente, desenvolvi uma API do lado do servidor para um protocolo de extensão http popular. Os requisitos eram:

  • Os clientes já estavam escritos e o código não podia ser alterado
  • Clientes diferentes têm comportamentos diferentes
  • O comportamento no http e https varia
  • O comportamento com diferentes tipos de autenticação varia
  • Até cem solicitações por segundo para cada cliente
  • Todos ignoram o RFC

Há só uma solução aqui: o analisador de pacotes.

Ou, se estou desenvolvendo uma API REST para consumo de JS, o cliente REST é uma ferramenta perfeita para testes.

Ao depurar o aplicativo da Web, comece com o navegador.

Na Parte 2, vamos discutir o que pode ser feito (muita coisa) para a depuração da Web no lado do Caché.

Quais são suas abordagens para depurar a comunicação no lado do cliente?

0
0 94
Artigo Danusa Calixto · Abr. 4, 2024 3m read

Olá, Desenvolvedores!

Suponha que você tenha uma classe persistente com dados e queira ter uma IU Angular simples para visualizar os dados e fazer operações CRUD.

Recentemente, @Alberto Fuentes descreveu como desenvolver uma IU Angular para seu aplicativo do InterSystems IRIS usando RESTForms2. 

Neste artigo, quero explicar a você como obter uma IU Angular simples para fazer operações CRUD e visualizar seus dados de classes do InterSystems IRIS automaticamente em menos de 5 minutos.

Vamos lá!

Para isso, você precisará do seguinte:

Usarei uma classe Data.Countries, que gerei e importei pelo csvgen usando este comando:

d ##class(community.csvgen).GenerateFromURL("https://raw.githubusercontent.com/datasciencedojo/datasets/master/WorldDBTables/CountryTable.csv",",","Data.Countries"

Para criar uma IU Angular, precisamos expor a API REST para essa classe, que servirá as operações CRUD.

Vamos usar o módulo restforms2 para isso. 

Esse comando no dockerfile instala restforms2 no contêiner IRIS:

zpm "install restforms2" \

Para adicionar uma API REST, precisamos derivar a classe de Form.Adaptor:

Class Data.Countries Extends (%Library.Persistent, Form.Adaptor)

Adicione os parâmetros restforms2 à classe persistente para gerenciar o comportamento geral: parâmetro de classificação, nome de exibição etc.:

// Nome do formulário, e não uma chave global, então pode ser qualquer coisa
Parameter FORMNAME = "Countries";

/// Permissões padrão /// Objetos desse formulário podem ser Criados, Lidos, Atualizados e Excluídos /// Redefina esse parâmetro para mudar as permissões para todo mundo /// Redefina o método checkPermission (veja Form.Security) para essa classe  /// para adicionar a segurança personalizada com base em usuário/funções/etc. Parameter OBJPERMISSIONS As %String = "CRUD";

/// Propriedade usada para informações básicas sobre o objeto /// Por padrão, o método getObjectDisplayName recebe seu valor dela Parameter DISPLAYPROPERTY As %String = "name";

Perfeito. Em seguida, podemos usar a sintaxe restforms2 para informar ao restforms2 quais propriedades queremos expor às operações CRUD. Você pode fazer isso adicionando o atributo "DISPLAYNAME =" às propriedades que você quer expor em restforms2-ui. Exemplo:

Property code As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 2 ];

Property name As %Library.String(DISPLAYNAME = "Name", MAXLEN = 250) [ SqlColumnNumber = 3 ];

Property continent As %Library.String(DISPLAYNAME = "Continent", MAXLEN = 250) [ SqlColumnNumber = 4 ];

Property region As %Library.String(DISPLAYNAME = "Region", MAXLEN = 250) [ SqlColumnNumber = 5 ];

Property surfacearea As %Library.Integer(DISPLAYNAME = "Surface Area", MAXVAL = 2147483647, MINVAL = -2147483648) [ SqlColumnNumber = 6, SqlFieldName = surface_area ];

Property independenceyear As %Library.Integer(DISPLAYNAME = "Independence Year", MAXVAL = 2147483647, MINVAL = -2147483648) [ SqlColumnNumber = 7, SqlFieldName = independence_year ];

Ótimo! Agora vamos introduzir a camada de UI.  Esse comando no dockerfile instala restforms2-ui, que é a IU Angular para Restform2:

zpm "install restforms2-ui" \

É isso! Vamos examinar a IU para sua classe, que você pode encontrar no URL server:port/restforms2-ui:

RESTForms são usados com as classes de teste Person e Company, e você pode usar isso para examinar os recursos de restformsUI. No momento, é possível editar campos de string, número, booleano, data e consulta.

Você pode testar tudo isso no seu laptop, se clonar e compilar este repositório:

docker-compose up -d --build

E abrir o URL:

localhost:port/restforms2-ui/index.html

ou, se você usar o VSCode, selecione esse item do menu:

Boa programação e fique ligado!

0
0 87
Anúncio Danusa Calixto · Mar. 20, 2024

Olá Desenvolvedores,

Junte-se a nós na próxima Mesa Redonda de Desenvolvedores em 26 de Março às 12 horas (BR - SP)📍
Teremos 2 temas abordados pelos especialistas convidados e discussão aberta como sempre!

Palestras:

Demonstração sobre documentação e teste de chamadas REST gerando documentação e fazendo cenários para testes de integração - apresentado por @Danny Wijnschenk , Desenvolvedor de Aplicações, Winfo.
➡ Cypress para testes de aplicativos da web  - apresentado por @Pravin Barton , Desenvolvedor de Aplicações Senior, InterSystems

Registre-se através >> desafio Global Masters <<

 

0
0 79
Artigo Danusa Calixto · jan 11, 2024 3m read

Sejam todos bem-vindos!

Neste breve artigo, quero apresentar um exemplo de uso que vários de vocês que trabalham com o IRIS como back-end para seus web applications devem ter enfrentado mais de uma vez: como enviar um arquivo do front-end para o servidor.

Em geral, a maneira mais simples que encontrei de realizar essa tarefa é transformar o arquivo do front-end para o formato Base64 e fazer uma chamada POST para o servidor anexando o Base64 obtido a uma mensagem JSON onde é indicado o nome do arquivo em um parâmetro e os dados codificados em outro. Algo parecido com isto:

{
    "fileData": "JVBERi0xLjQKJdPr6eEKMSAwIG...",
    "fileName": "example.pdf"
}

Da minha instância IRIS, configurei um web application para gerenciar essas chamadas POST de uma classe que estende %CSP.REST. Essa classe tem um classmethod para gerenciar o POST corretamente. O código deve ficar assim:

ClassMethod SaveFile() As%Status
{
    Try {
        Do##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit// Lendo o corpo da chamada http com os dados do arquivo
    <span class="hljs-keyword">set</span> dynamicBody = {}.<span class="hljs-built_in">%FromJSON</span>(<span class="hljs-built_in">%request.Content</span>)

    <span class="hljs-keyword">set</span> dynamicStream = dynamicBody.<span class="hljs-built_in">%Get</span>(<span class="hljs-string">"fileData"</span>,,<span class="hljs-string">"stream&lt;base64"</span>)
    
    <span class="hljs-keyword">set</span> stream=<span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%Stream.FileBinary</span>).<span class="hljs-built_in">%New</span>()
    <span class="hljs-keyword">set</span> sc=stream.LinkToFile(<span class="hljs-string">"/shared/durable/"</span>_dynamicBody.fileName)
    <span class="hljs-keyword">set</span> sc=stream.CopyFromAndSave(dynamicStream)
   
} <span class="hljs-keyword">Catch</span> (ex) {
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"400"</span>)
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%WriteResponse</span>(ex.DisplayString())
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"errormessage"</span>: <span class="hljs-string">"Client error"</span>}
}
<span class="hljs-keyword">Quit</span> <span class="hljs-built_in">$$$OK</span>

}

Vamos explicar em detalhes o que esse método está fazendo:

  1. Recebemos o conteúdo enviado no corpo da mensagem e transformamos isso em um %Library.DynamicAbstractObject que pode ser lido como um objeto.
    set dynamicBody = {}.%FromJSON(%request.Content)

     

  2. Essa etapa é onde a mágica acontece. Transformamos nosso Base64 em um tipo %Stream que podemos obter usando o método %Get da classe %Library.DynamicObject.
  3. set dynamicStream = dynamicBody.%Get("fileData",,"stream<base64")
    Como você pode ver, estamos passando como um parâmetro de entrada o tipo de transformação que queremos realizar, em nosso exemplo de Base64 para Stream (stream<base64). Qual é a vantagem de usar essa instrução? Bem, não vamos estar limitados pelo valor MAXSTRING, ou seja, se nosso arquivo em Base64 for muito grande, nunca vamos receber uma exceção porque ultrapassamos o limite máximo de comprimento permitido para uma String.
  4. Por fim, criamos em nosso servidor o arquivo que conterá os dados enviados em Base64 com o nome definido na chamada recebida e escrevemos nosso objeto de tipo %Stream nele.
    set stream=##class(%Stream.FileBinary).%New()
    set sc=stream.LinkToFile("/shared/durable/"_dynamicBody.fileName)
    set sc=stream.CopyFromAndSave(dynamicStream)

Como você pode ver, é muito fácil gerenciar arquivos em Base64 do nosso servidor. Se você tiver dúvidas, não hesite em escrevê-las na seção de comentários. Será um prazer responder.

0
0 141
Artigo Danusa Calixto · Nov. 27, 2023 11m read

Eu estava tentando encontrar uma solução para conceder aos clientes acesso anônimo a determinados endpoints de API e também proteger outros endpoints na minha API REST. No entanto, ao definir um Web App, você só pode proteger o aplicativo inteiro, e não partes específicas.

Procurei respostas na comunidade, mas não encontrei nenhuma solução exata, exceto uma recomendação para criar dois Web Apps separados, um protegido e outro não. No entanto, na minha opinião, essa estratégia requer muito trabalho e cria um overhead de manutenção desnecessário. Prefiro desenvolver minhas APIs começando pela especificação e decidir nela quais endpoints devem permitir o acesso anônimo ou não.

Neste artigo, forneço dois exemplos: um para a Autenticação Básica e outro para o JWT, que é usado no contexto do OAuth 2.0. Se você notar alguma falha nestes exemplos, me avise e farei as correções necessárias.

Pré-requisitos

Primeiro, defina um Web App para sua API REST. Configure-o para o acesso não autenticado e especifique os privilégios necessários para o aplicativo. Especifique apenas as funções e os recursos necessários para o sucesso do uso da API.

Crie uma classe, por exemplo, REST.Utils, onde você implementará os classmethods helper que verificam as credenciais.

Class REST.Utils  
{

}

Autenticação Básica

Se você quiser um endpoint seguro usando a Autenticação Básica, use o seguinte método para verificar se o nome de usuário e a senha informados no cabeçalho da Autorização HTTP têm os privilégios corretos para acessar o endpoint de API restrito.

/// Confira se os usuários têm as permissões necessárias.
/// - auth: o cabeçalho da Autorização.
/// - resource: o recurso para verificar as permissões.
/// - permissions: as permissões verificadas.
/// 
/// Exemplo:
/// > Do ##class(REST.Utils).CheckBasicCredentials(%request.GetCgiEnv("HTTP_AUTHORIZATION", ""), "RESOURCE", "U")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckBasicCredentials(auth As %String, resource As %String, permissions As %String) As %Status
{
  /// Analise a sanidade da entrada  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Confira se o cabeçalho da autorização começa com Basic  
  if ($FIND(auth, "Basic") > 0) {
    /// Retire a parte "Basic" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  /// Decodifique o nome de usuário e a senha codificados em base64  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth, ":", 1)
  Set password = $PIECE(auth, ":", 2)

  /// Tente fazer login como o usuário informado no cabeçalho da Autorização  
  Set tStatus = $SYSTEM.Security.Login(username, password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Confira se o usuário tem as permissões necessárias  
  Set tStatus = $SYSTEM.Security.Check(resource, permissions)

  /// Retorne o status. Se o usuário tiver as permissões necessárias, o status será $$$OK  
  Return tStatus
}

No endpoint que você quer proteger, chame o método CheckBasicCredentials e confira o valor retornado. Um valor de retorno 0 indica a falha na verificação. Nesses casos, retornamos HTTP 401 de volta ao cliente.

O exemplo abaixo verifica se o usuário tem o recurso SYSTEM_API definido com privilégios USE. Se não tiver, retorne HTTP 401 ao cliente. Lembre-se de que o usuário da API precisa ter o privilégio %Service_Login:USE para usar o método Security.Login.

Exemplo

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckBasicCredentials(authHeader, "SYSTEM_API", "U")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... resto do código

JWT

Em vez de usar a Autenticação Básica para proteger um endpoint, prefiro usar os Tokens de Acesso JWT do OAuth 2.0, já que são mais seguros e oferecem uma maneira mais flexível de definir privilégios por escopos. O seguinte método verifica se o token de acesso JWT fornecido no cabeçalho da Autorização HTTP tem os privilégios corretos para acessar o endpoint de API restrito.

/// Verifique se o JWT fornecido é válido.
/// - auth: o cabeçalho da Autorização.
/// - scopes: os escopos que esse token JWT deve ter.
/// - oauthClient: o cliente OAuth que é usado para validar o token JWT. (opcional)
/// - jwks: o JWKS usado para a validação da assinatura do token (opcional)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Analise a sanidade da entrada  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }
  
  /// Confira se o cabeçalho da autorização começa com Bearer. Se sim, faça a limpeza do token.  
  if ($FIND(token, "Bearer") > 0) {
    /// Retire a parte "Bearer" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Crie uma lista da string de escopos  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Remova os espaços em branco de cada escopo  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decodifique o token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Obtenha o epoch time atual
  Set now = $ZDATETIME($h,-2)

  /// Confira se o token expirou  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Confira se o token tem os escopos necessários
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// Se o token é válido em todo o escopo e não expirou, confira se a assinatura é valida
  if (oauthClient '= "") {
    /// Se especificamos um cliente OAuth, use isso para validar a assinatura do token
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// Se especificamos um JWKS, use isso para validar a assinatura do token
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}

/// Decodifique um token JWT.
/// - token: o token JWT a decodificar.
/// - payload: o payload do token JWT. (Saída)
/// - header: o cabeçalho do token JWT. (Saída)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decodifique e processe o Cabeçalho  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decodifique e processe o Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}

Novamente, no endpoint que você quer proteger, chame o método CheckJWTCredentials e confira o valor retornado. Um valor de retorno 0 indica a falha na verificação. Nesses casos, retornamos HTTP 401 de volta ao cliente.

O exemplo abaixo verifica se o token tem os escopos scope1 e scope2 definidos. Se não tiver os escopos necessários, houver expirado ou falhar na validação da assinatura, ele retornará um código de status HTTP 401 ao cliente.

Exemplo

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckJWTCredentials(authHeader, "scope1,scope2")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... resto do código

Conclusão

Aqui está o código completo para a classe REST.Utils. Se você tiver quaisquer sugestões sobre como melhorar o código, me informe. Atualizarei o artigo devidamente.

Uma melhoria óbvia seria verificar a assinatura JWT para garantir que é válida. Para fazer isso, você precisa ter a chave pública do emissor.

Class REST.Utils
{
  
/// Confira se os usuários têm as permissões necessárias.
/// - auth: o conteúdo do cabeçalho da Autorização.
/// - resource: o recurso para verificar as permissões.
/// - permissions: as permissões verificadas.
/// 
/// Exemplo:
/// > Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckBasicCredentials(authHeader, "RESOURCE", "U"))
/// 
/// Retorne: %Status. O status da verificação.  
ClassMethod CheckBasicCredentials(authHeader As %String, resource As %String, permissions As %String) As %Status
{
  Set auth = authHeader

  /// Analise a sanidade da entrada  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Confira se o cabeçalho da autorização começa com Basic  
  if ($FIND(auth, "Basic") > 0) {
    // Retire a parte "Basic" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  Try {
  /// Decodifique o nome de usuário e a senha codificados em base64  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth,":",1)
  Set password = $PIECE(auth,":",2)
  } Catch {
    Return $$$ERROR($$$GeneralError, "Not a valid Basic Authorization header")
  }

  /// Tente fazer login como o usuário informado no cabeçalho da Autorização  
  Set tStatus = $SYSTEM.Security.Login(username,password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Confira se o usuário tem as permissões necessárias  
  Set tStatus = $SYSTEM.Security.Check(resource, permissions)

  /// Retorne o status. Se o usuário tiver as permissões necessárias, o status será $$$OK  
  Return tStatus
}

/// Verifique se o JWT fornecido é válido.
/// - auth: o cabeçalho da Autorização.
/// - scopes: os escopos que esse token JWT deve ter.
/// - oauthClient: o cliente OAuth que é usado para validar o token JWT. (opcional)
/// - jwks: o JWKS usado para a validação da assinatura do token (opcional)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Analise a sanidade da entrada  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }
  
  /// Confira se o cabeçalho da autorização começa com Bearer. Se sim, faça a limpeza do token.  
  if ($FIND(token, "Bearer") > 0) {
    /// Retire a parte "Bearer" do cabeçalho de Autorização e remova os espaços à esquerda e à direita.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Crie uma lista da string de escopos  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Remova os espaços em branco de cada escopo  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decodifique o token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Obtenha o epoch time atual
  Set now = $ZDATETIME($h,-2)

  /// Confira se o token expirou  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Confira se o token tem os escopos necessários
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// Se o token é válido em todo o escopo e não expirou, confira se a assinatura é valida
  if (oauthClient '= "") {
    /// Se especificamos um cliente OAuth, use isso para validar a assinatura do token
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// Se especificamos um JWKS, use isso para validar a assinatura do token
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}


/// Decodifique um token JWT.
/// - token: o token JWT a decodificar.
/// - payload: o payload do token JWT. (Saída)
/// - header: o cabeçalho do token JWT. (Saída)
/// 
/// Exemplo:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Retorne: %Status. O status da verificação.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decodifique e processe o Cabeçalho  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decodifique e processe o Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}
}
0
0 141