Search

Clear filter
Announcement
Evgeny Shvarov · Aug 13, 2020

Technology Bonuses for the InterSystems FHIR Programming Contest

Hi Developers! Here're the technology bonuses for the FHIR programming contest that will give you extra points in the voting: FHIR Server REST API usage The usage of SQL Schema of FHIR Resources Healthcare standards data transformation Docker container usage See the details below. FHIR Server REST API usage - 1 point You get the bonus if you use the REST API endpoint of FHIR Server in InterSystems IRIS for health. You can take the IRIS-FHIR-Template which prepares the FHIR server during the docker image building. The documentation for FHIR API 4.0.1 could be found here. Learn more in InterSystems IRIS for Health documentation. FHIR SQL Schema usage - 1 point You can collect this technology bonus if you use FHIR SQL Schema in the SQL queries of your application. You can use this schema e.g. for making FHIR Analytics solution. Use HSFHIR_I0001_R for resources schema for full resources and HSFHIR_I0001_S schema to search with SQL for resources. Check the examples in the template. Healthcare standards transformations - 1 point InterSystems IRIS for Health contains Healthcare Interoperability modules which help to perform data transformations from different healthcare standards to FHIR and vice-versa. Make CDA to FHIR, HL7v2 to FHIR, or any other transformations in your application to collect this bonus. See the examples of HL7v2 to FHIR and CDA to FHIR transformations. Learn more in the documentation. Docker container usage - 1 point The application gets a 'Docker container' bonus if it uses InterSystems IRIS running in a docker container. The IRIS-FHIR-Template uses docker so you can collect this bonus if you build your solution using these templates. So! Feel free to ask any questions about using the listed technologies. Good luck in the competition! We analyzed the projects submitted for the contest and here is the matrix, which project gets what bonus: Name FHIR Server REST API usage FHIR SQL Schema usage Healthcare standards transformations Docker container usage iris-fhir-portal x x x iris4health-fhir-analytics x x x x fhir-chatbot x x QR-FHIR x x REST_API_with_FHIR_data_scheme x x Update: iris4health-fhir-analytics uses FHIR REST API too. iris-fhir-portal uses FHIR SQL Schema now. More details.
Article
Yuri Marx · Dec 22, 2020

InterSystems DC Past Contests - a successful journey

I compiled some data about past InterSystems DC constest. Some statistics: 64 applications (17% of all current apps into OEX - 377 apps in december/2020) 08 contests 29 developers See the summary: Contest Winner Apps Total Apps Benefits to the community InterSystems IRIS, Docker and ObjectScript BlocksExplorer, ISC DEV, sql-builder, isc-generate-db, declarative-objectscript 20 New open source utilities to maintain and develop SQL and admin Caché and IRIS InterSystems IRIS with REST API iris-history-monitor, Production Manager, JSON-Filter, simple-spellchecker 7 New utilities to admin and manage IRIS Interoperability productions InterSystems IRIS Native API iris-python-suite, WebSocket Client JS, ObjectScript Kernel 8 How to use IRIS with Python, Node.js and Java and new utilities to change data with IRIS Native API InterSystems IRIS AI iris-integratedml-monitor-example, iris-ml-suite, ESKLP, SAPPHIRE 4 How to use Python and AutoML to do IA and utilities to do IA with IRIS InterSystems IRIS for Health FHIR iris-fhir-portal, iris4health-fhir-analytics, fhir-chatbot 5 How to use IRIS do to FHIR healthcare advanced analytics and see operational FHIR records InterSystems Full Stack npm-iris, apptools-admin, realworld-intersystems-iris, restoreUI 9 Full stack samples using IRIS, Angular, HTML5 and database utilities InterSystems Interoperability Open API Client Gen, OCR Service, IRIS Interoperability Message Viewer 9 Utility to generate productions from Swagger, Advanced UI for productions and OCR support to IRIS using PEX InterSystems Analytics iris-analytics-notebook, website-analyzer, iris-analytics-package 4 Execute IRIS analysis sentences into notebooks, do NLP into website content and create your dashboard and cubes from a CSV in seconds. Developers: Aleksandr Kalinin, alex kosinets, Alexey Maslov, Alexey Nechaev, davi massaru teixeira muta, David Crawford, Dmitrii Kuznetsov, Dmitry Maslennikov, Gevorg Arutiunian, Henrique Gonçalves Dias, Henry Hamon Pereira, José Roberto Pereira Jr, Lorenzo Scalese, Maks Atygaev, Mark Erwin Villarina, Muni Ganesh, Nikita Mullin,Nikolay Soloviev, Oleh Dontsov, Oliver Wilms,Renato Banzai, Rob Tweed, Robert Cemper,Rubens Silva, Sergey Kamenev,Sergey Mikhailenko,Vasiliy Bondar, YURI MARX PEREIRA GOMES. The DC Team: @Evgeny.Shvarov, @Irina.Podmazko, @Olga.Zavrazhnova2637, @Anastasia.Dyubaylo and some ISC sales engineers like @Eduard.Lebedyuk supporting this fantastic team, was very nice to organize these fantastic contests. Amazing post! Thank you so much, Yuri! This is very pleasant! Thanks @Evgeny.Shvarov Wow! Yuri! Thaaaaank you! 😍 Our team is very touched by your feedback and this post! Yuri, thank you very much for such pleasant post!🤩 Thanks @Anastasia.Dyubaylo and @Iryna_Mologa Awesome summary! So great to see the numbers, all the names of developers, applications, benefits! 🤩 Thank you, Yuri, that's so valuable! Thanks @Olga.Zavrazhnova2637 !
Announcement
Evgeny Shvarov · Dec 25, 2020

Technology Bonuses for Projects of InterSystems Analytics Contest

Hi developers! We had a set of technology bonuses for the InterSystems Analytics contest. And here is how projects collected it: Project docker zpm IRIS BI IRIS NLP Integrated ML Real Data ISC Reports Total Nominal 1 1 1 1 1 1 2 8 iris-analytics-notebook 1 1 1 3 website-analyzer 1 1 1 1 4 dataking-server 1 1 2 iris-analytics-package 1 1 1 2 5 This table is subject to change upon project improvements or if we missed something. Fill free to claim the bonus in the comments here or on discord. Stay tuned! ZPM bonus is added for iris-analytics-package InterSystems Reports bonus is added for iris-analytics-package
Announcement
Evgeny Shvarov · Dec 18, 2020

Meet A New Voting Engine For InterSystems Contests!

Hi Developers, You asked for it – we did it! We are introducing a new voting engine and algorithm to InterSytems Contests which you can try with the current contest – starting next Monday! You can select 3 projects now: the 1st, the 2nd, and the 3rd place upon your decision. This is how it works for the Community leaderboard: Community Leaderboard: Place Points 1st 3 2nd 2 3rd 1 And there will be more complex math for Experts leaderboard, where different levels of experts have more "points" power: Experts Leaderboard: Level Place 1st 2nd 3rd VIP level in GM, Moderators, Product Managers 9 6 3 Expert level in Global Masters 6 4 2 Specialist level in Global Masters 3 2 1 Experts' votes will also contribute 3-2-1 points to the Community leaderboard too. This is how it works: To take part in the voting, you need: Sign in to Open Exchange – DC credentials will work. Make any valid contribution to Developer Community – answer or ask questions, write an article, comment on any post, and you'll see vote buttons available. We hope this new system will be fairer and you'll have the option to give your votes to more projects that you like. Comments, suggestions are welcome! Excellent news! This is a long-awaited improvement A GREAT improvement! Though a challenge to the voters: They have to make a decision !
Announcement
Anastasia Dyubaylo · Dec 21, 2020

Vote for the best app in the InterSystems Analytics Contest!

Hey Developers, This week is a voting week for the InterSystems Analytics Contest! So, it's time to give your vote to the best solutions built with InterSystems IRIS. 🔥 You decide: VOTING IS HERE 🔥 How to vote? Please meet the new voting engine and algorithm for the Experts and Community nomination: You can select 3 projects: the 1st, the 2nd, and the 3rd place upon your decision. This is how it works for the Community leaderboard: Community Leaderboard: Place Points 1st 3 2nd 2 3rd 1 And there will be more complex math for Experts leaderboard, where different levels of experts have more "points" power: Experts Leaderboard: Level Place 1st 2nd 3rd VIP level in GM, Moderators, Product Managers 9 6 3 Expert level in Global Masters 6 4 2 Specialist level in Global Masters 3 2 1 Experts' votes will also contribute 3-2-1 points to the Community leaderboard too. Voting Voting takes place on the Open Exchange Contest Page and you need to sign in to Open Exchange – you can do it with your DC account credentials. If you changed your mind, cancel the choice and give your vote to another application – you have 7 days to choose! Contest participants are allowed to fix the bugs and make improvements to their applications during the voting week, so don't miss and subscribe to application releases! ➡️ Also, please check out the new voting rules for InterSystems online contest here. Hey Developers, After the first day of the voting we have: Expert Nomination, Top 3 iris-analytics-notebook – 3 website-analyzer – 2 dataking-server – 1 ➡️ The leaderboard. Community Nomination, Top 3 iris-analytics-notebook – 14 website-analyzer – 11 iris-analytics-package – 11 ➡️ The leaderboard. Experts, we are waiting for your votes! 🔥 Participants, improve & promote your solutions! Developers! Our participants need your votes! 📢 Please follow our leaderboard and choose the application you like!✌️ Developers! Only 2 days left before the end of voting. Please check out the Contest Board and vote for the solutions you like! 👍🏼
Announcement
Anastasia Dyubaylo · Dec 28, 2020

Online Meetup with the InterSystems Analytics Contest Winners

Hi Community, We're pleased to invite you to the online meetup with the winners of the InterSystems Analytics Contest! Date & Time: Monday, January 4, 2021 – 10:00 EDT What awaits you at this virtual Meetup? Our winners' bios. Short demos on their applications. An open discussion about technologies being used, bonuses, questions. Plans for the next contests. Our speakers: @José.Pereira, Business Intelligence Developer at Shift Consultoria e Sistemas Ltda @Yuri.Gomes, Software Architect at YM Consulting @Henrique, System Management Specialist / Database Administrator, Sao Paulo Federal Court @Evgeny.Shvarov, InterSystems Developer Ecosystem Manager @Carmen.Logue, InterSystems Product Manager - Analytics and AI You will also have the opportunity to ask any questions to our developers in a special webinar chat. We will be happy to talk to you at our Virtual Meetup! ➡️ Join us using this link! Developers! InterSystems Product Manager @Carmen.Logue will also join our virtual meetup on Monday, Jan 4! 👉🏼 Register today! Hey Developers! The recording of this virtual meetup is already on InterSystems Developers YouTube: ⏯ Online Meetup with the InterSystems Analytics Contest Winners Big applause to all the speakers! 👏🏼 Great session, great apps
Article
Mihoko Iijima · Mar 5, 2021

[InterSystems IRIS for the first time] Interoperability: Message

**This article is a follow-up to this post**. In the previous article, [What is a Production?], we checked the production contents. We ran the sample code and checked the flowing messages' contents into the production on the Visual Trace page. This article will review the concept and the definition of the ** messages** used to send and receive data between components from the required development content for system integration. * Production[ previous post ] * Message * Components * Business Services * Business Processes * Business Operations Before creating a message, let's review the case study. > A company operates a shopping site and is changing the order of displaying product information to match the seasons.However, some items sell well regardless of the season, while others sell at unexpected times, which does not match the current display rule of changing the order.Therefore, we looked into the possibility of changing the order of display to match the day's temperature instead of the season. It became necessary to survey the temperature of the purchased products at that time. Since an external Web API is available for checking weather information, we plan to collect the weather information at the time of purchase and register it in the later review database. From this case, you can see the following: Information to be received from the outside is "purchased product name and name of the city."The information to be sent from IRIS to an external system to request processing is the "name of the city." The result of this process will be "weather information" for the city used as an input. ![image](/sites/default/files/inline/images/image1128.png) From this case study, we will implement the components needed for system integration. But before that, to run the components, you need to send and receive messages, which are relay data, and to use the messages, you need to define the message class. A message class is designed to consider **what information (i.e., messages) should be sent and received to make the component to run.** In this process, we need the following two types of information: A) The name of the city to send to an external Web API to obtain weather information. B) weather information and purchased product name for recording in the DB. The name of the city in A) and the **purchased product name** in B) can be included in the input information to IRIS. The weather information in B) can be retrieved from the response information of an external Web API. Here is a diagram that considers what information would be needed to send and receive each component from the available data. The first line of the ** yellow balloons** describes the message class name, and the second line onwards states what to set for the property. ![image](/sites/default/files/inline/images/image1068jp.png) In the sample code, we have the following three types of **messages**: Start.Request(Request message)It is used to send the name of the purchased product and the city to acquire the weather information. Start.Response(Response message)They are used to return the results of operations (weather information) to obtain weather information. Start.InsertRequest(Request message) It is used to send the weather information and the name of the purchased products for DB registration. The messages are specified in a superclass, Request message, and Response message are derived from **Ens.Request** and **Ens.Response**, respectively. The following is an example of the definition of the Request message Start.Request. ![](https://jp.community.intersystems.com/sites/default/files/inline/images/images/image(1069).png) Below is an example of the definition of the Response message. ![](https://community.intersystems.com/sites/default/files/inline/images/images/image(1075).png) The Request message, Start.InsertRequest to be sent with the DB registration request is as follows: (We plan to set the WeatherInfo property to the information in Start.Response, which will be returned after the weather information is obtained.) ![](https://jp.community.intersystems.com/sites/default/files/inline/images/images/image(1076).png) If you want to create it in Studio, you can also use the Message Creation Wizard. Reference) Steps to create a response class in Studio The key point so far A message class is designed with the idea of "what information (i.e., messages) should be sent and received" to make the component RUN. Once the message class (which is the information to drive each component) is implemented, the next step is to create a class for the component.
Question
Ponnumani Gurusamy · May 4, 2020

Didn't able to start InterSystems Cache

Hi Team, We weren't able to start the InterSystems cache server. I attached error screenshot and console log details on below. Console log error: Retry CLNDMN job start after timeout with last job error = <UNKNOWN ERROR>.JOBTRACE: Create job indirect, rtnref = START^CLNDMN.JOBTRACE: Create job by control, rtnref = START^CLNDMN, pid = 6944.JOBTRACE: Timed out waiting for child (6944) to appear in PIDTAB.Error: ERROR #5001: Cannot start CLNDMN job - Shutting down the system Please guide me, how we resolve this issue. Thanks & Regards, Ponnumani G. First, check if the Caché SERVICE (in win sense) gets started as account With enough privileges Next, check that the installation directory and all sub dir are accessible with all right from this account Eventually, consider a reinstall / Update with enough ADMINISTRATOR rights We tried with administrator credential also. But we are facing this same issue.
Article
Yuri Marx · Jun 3, 2020

Integrate SAP and InterSystems IRIS using OData

SAP offers a broad support to OData in all your products. So, OData can be an excellent option to exchange data between SAP and InterSystems IRIS. Follow the instructions in the article https://community.intersystems.com/post/odata-and-intersystems-iris to expose your IRIS data as REST OData services. To consume InterSystems IRIS data from SAP using OData, follow these steps (credits to the next steps of this tutorial: https://sapyard.com/sapui5-for-abapers-consuming-odata-service-from-sapui5-application-crud-operations/) : Create a new SAPUI5 application by the name crud_demo. Create a XML view ‘crud_demo.view’. Write below code in it. <core:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" controllerName="crud_demo.crud_demo" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:l="sap.ui.commons.layout"> <Page title="CRUD Operations"> <content> <l:AbsoluteLayout width="10rem" height="10rem"></l:AbsoluteLayout> <VBox xmlns="sap.m" id="vboxid"> <items> <HBox xmlns="sap.m"> <items> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Button xmlns="sap.m" id="cbtn" press="oDataCall" text="Create"></Button> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Button xmlns="sap.m" id="rbtn" press="oDataCall" text="Read"></Button> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Button xmlns="sap.m" id="ubtn" press="oDataCall" text="Update"></Button> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Button xmlns="sap.m" id="dbtn" press="oDataCall" text="Delete"></Button> </items> </HBox> <HBox xmlns="sap.m"> <items> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Input xmlns="sap.m" id="uniqueid" placeholder="ID" value="1"></Input> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Input xmlns="sap.m" id="nameid" placeholder="Name" value="test"></Input> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Input xmlns="sap.m" id="emailid" placeholder="Email" value="test@gmail.com"></Input> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Input xmlns="sap.m" id="mobid" placeholder="Mobile" value="8888888888"></Input> </items> </HBox> <HBox xmlns="sap.m"> <items> <l:AbsoluteLayout width="20px" height="20px"></l:AbsoluteLayout> <Table xmlns="sap.m" id="userdatatable" headerText="User Data"> <items> <ListItemBase xmlns="sap.m" id="id1"></ListItemBase> </items> <columns> <!-- sap.m.Column --> <Column xmlns="sap.m"> <header> <Text xmlns="sap.m" text="Id" ></Text></header></Column> <Column xmlns="sap.m"> <header> <Text xmlns="sap.m" text="Name" ></Text></header></Column> <Column xmlns="sap.m"> <header> <Text xmlns="sap.m" text="Email" ></Text></header></Column> <Column xmlns="sap.m"> <header> <Text xmlns="sap.m" text="Mobile" ></Text></header></Column> </columns> </Table> </items> </HBox> </items> <!-- sap.ui.core.Control --> </VBox> </content> </Page> </core:View> Create crud_demo.controller.js. Write below code in it. onInit: function() { that = this; // Create Model Instance of the oData service var oModel = new sap.ui.model.odata.v2.ODataModel("/sap/opu/odata/sap/ZCRUD_DEMO_SRV"); sap.ui.getCore().setModel(oModel, "myModel"); }, oDataCall:function(oEvent) { // call oData service's function based on which button is clicked. debugger; var myModel = sap.ui.getCore().getModel("myModel"); myModel.setHeaders({ "X-Requested-With" : "X" }); // CREATE****************** if ('Create' == oEvent.oSource.mProperties.text) { var obj = {}; obj.id = that.getView().byId("uniqueid").getValue(); obj.name = that.getView().byId("nameid").getValue(); obj.email = that.getView().byId("emailid").getValue(); obj.mobile = that.getView().byId("mobid").getValue(); myModel.create('/userdataSet', obj, { success : function(oData, oResponse) { debugger; alert('Record Created Successfully...'); }, error : function(err, oResponse) { debugger; alert('Error while creating record - ' .concat(err.response.statusText)); } }); } // READ****************** else if ('Read' == oEvent.oSource.mProperties.text) { var readurl = "/userdataSet?$filter=(id eq '')"; myModel.read(readurl, { success : function(oData, oResponse) { debugger; var userdata = new sap.ui.model.json.JSONModel({ "Result" : oData.results }); var tab = that.getView().byId("userdatatable"); tab.setModel(userdata); var i = 0; tab.bindAggregation("items", { path : "/Result", template : new sap.m.ColumnListItem({ cells : [ new sap.ui.commons.TextView({ text : "{id}", design : "H5", semanticColor : "Default" }), new sap.ui.commons.TextView({ text : "{name}", design : "H5", semanticColor : "Positive" }), new sap.ui.commons.TextView({ text : "{email}", design : "H5", semanticColor : "Positive" }), new sap.ui.commons.TextView({ text : "{mobile}", design : "H5", semanticColor : "Positive" }), ] }) }); }, error : function(err) { debugger; } }); } // UPDATE****************** if ('Update' == oEvent.oSource.mProperties.text) { var obj = {}; obj.id = that.getView().byId("uniqueid").getValue(); obj.email = that.getView().byId("emailid").getValue(); var updateurl = "/userdataSet(id='" + that.getView().byId("uniqueid").getValue() + "')"; myModel.update(updateurl, obj, { success : function(oData, oResponse) { debugger; alert('Record Updated Successfully...'); }, error : function(err, oResponse) { debugger; alert('Error while updating record - ' .concat(err.response.statusText)); } }); } // DELETE****************** if ('Delete' == oEvent.oSource.mProperties.text) { var delurl = "/userdataSet(id='" + that.getView().byId("uniqueid").getValue() + "')"; myModel.remove(delurl, { success : function(oData, oResponse) { debugger; alert('Record Removed Successfully...'); }, error : function(err, oResponse) { debugger; alert('Error while removing record - ' .concat(err.response.statusText)); } }); } } Save, Deploy and Run the application. You should be able to run the application using below URL http://hostname:8000/sap/bc/ui5_ui5/sap/zcrud_demo/index.html. Output:
Announcement
Evgeny Shvarov · Sep 14, 2020

Technology Bonuses for the InterSystems Full Stack Contest

Hi Developers! Here're the technology bonuses for the Full Stack Contest that will give you extra points in the voting: InterSystems IRIS REST API Usage InterSystems Native API Usage InterSystems JDBC Usage Unit Testing ZPM Package deployment Docker container usage See the details below. InterSystems IRIS REST API usage - 1 point You get the bonus if you access InterSystems IRIS via REST API in your Full Stack application. You either can build the REST API by yourself, or use any built-in or install it via ZPM. Learn more on InterSystems REST API. InterSystems Native API usage - 1 point You get this bonus if you access the data in your Full Stack application using any of the InterSystems Native API options: .NET, Java, Python, Node.js. Learn more here. InterSystems JDBC usage - 1 point InterSystems IRIS provides JDBC driver to access the data. You get the bonus if you refer to the data in your Full Stack application using SQL and InterSystems JDBC. Documentation. Unit Testing - 1 point Applications that have Unit Testing for the InterSystems IRIS code will collect the bonus. Learn more about ObjectScript Unit Testing in Documentation and on Developer Community. ZPM Package deployment - 1 point You can collect the bonus if you build and publish the ZPM(ObjectScript Package Manager) package for your Full Stack application so it could be deployed with: zpm "install your-full-stack-solution-name" command on IRIS with ZPM client installed. ZPM client. Documentation. Docker container usage - 1 point The application gets a 'Docker container' bonus if it uses InterSystems IRIS running in a docker container. You can collect the bonus if you use any of the following docker templates: Basic InterSystems IRIS Docker template IRIS REST API template Native API template IntegratedML template IRIS Analytics template So! Feel free to ask any questions about using the listed technologies. Good luck in the competition! Is Native API for ObjectScript also included for Bonus Points ? You've just posted an article!) I wasn't aware too. We need to check and maybe we'll include this as well! We may add more bonuses - the contest will start next Monday, we still have time to add more ;) until a few minutes before I wasn't aware myself Unit Testing bonus is introduced. I suggest more two bonus points. 1 - If you use BPL or integration adapter; 2 - If you use iris analytics, consuming bi cube from the frontend using mdx, api or sql.
Announcement
Anastasia Dyubaylo · Dec 5, 2020

InterSystems Analytics Contest Kick-off Webinar

Hi Community! We are pleased to invite all the developers to the upcoming InterSystems Analytics Contest Kick-off Webinar! The topic of this webinar is dedicated to the Analytics contest. On this webinar, we’ll demo the iris-analytics-template and answer the questions on how to develop, build, and deploy Analytics applications using InterSystems IRIS. Date & Time: Monday, December 7 — 12:00 PM EDT Speakers: 🗣 @Carmen.Logue, InterSystems Product Manager - Analytics and AI🗣 @Evgeny.Shvarov, InterSystems Developer Ecosystem Manager So! We will be happy to talk to you at our webinar! ✅ JOIN THE KICK-OFF WEBINAR! We're starting! Please join us here! Do you have this webinar record? My Internet down and I did not watch the end. Hey Developers! The recording of this webinar is available on InterSystems Developers YouTube! Please welcome: ⏯ InterSystems Analytics Contest Kick-off Webinar Big applause to our speakers! 👏🏼 And thanks to everyone for joining our webinar! Just published! Please check the video in the comments below 😉
Article
Guillaume Rongier · Mar 26, 2021

IAM (InterSystems API Manager), Zero to Hero

![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/introduction.png "Credit : https://blog.octo.com/kong-le-gorille-de-lapi-management-vu-de-pres/") This article contains the materials, examples, exercises to learn the basic concepts of IAM. You have all resources available on this git : https://github.com/grongierisc/iam-training. Solutions are in [training branch](https://github.com/grongierisc/iam-training/tree/training). This article will cover the following points : - [1. Introduction](#1-introduction) - [1.1. What is IAM ?](#11-what-is-iam-) - [1.2. What is an API Management ?](#12-what-is-an-api-management-) - [1.3. IAM Portal](#13-iam-portal) - [1.4. Flow of this training](#14-flow-of-this-training) - [2. Installation](#2-installation) - [2.1. What do you need to install?](#21-what-do-you-need-to-install) - [2.2. How IAM works with IRIS](#22-how-iam-works-with-iris) - [2.3. Setup](#23-setup) - [2.4. Install IAM](#24-install-iam) - [2.4.1. Iris Image](#241-iris-image) - [2.4.2. IAM Image](#242-iam-image) - [2.4.3. Update the docker file](#243-update-the-docker-file) - [2.4.4. Update the docker-compose](#244-update-the-docker-compose) - [2.4.5. Option : add IRIS_PASSWARD as .env](#245-option--add-iris_passward-as-env) - [2.4.6. Test it !](#246-test-it-) - [3. First Service/Route](#3-first-serviceroute) - [3.1. Create a service](#31-create-a-service) - [3.2. Create a route](#32-create-a-route) - [3.3. Test it !](#33-test-it-) - [4. Second, go further with plugin](#4-second-go-further-with-plugin) - [4.1. Add a plugin to the service](#41-add-a-plugin-to-the-service) - [4.2. Test it !](#42-test-it-) - [5. Third, add our own authentication](#5-third-add-our-own-authentication) - [5.1. Add consumers](#51-add-consumers) - [5.2. Add Basic auth plugin](#52-add-basic-auth-plugin) - [5.3. Add ACL Plugin](#53-add-acl-plugin) - [5.4. Configure USER with ACL and credentials](#54-configure-user-with-acl-and-credentials) - [5.5. Test it !](#55-test-it-) - [6. Exercice, Rate-Limiting](#6-exercice-rate-limiting) - [6.1. Solution](#61-solution) - [7. Dev Portal](#7-dev-portal) - [7.1. Overview](#71-overview) - [7.2. Enable it !](#72-enable-it-) - [7.3. Add your first spec](#73-add-your-first-spec) - [7.4. Test it !](#74-test-it-) - [7.5. Exercise](#75-exercise) - [7.5.1. Solution](#751-solution) - [8. Dev Portal, Part two, Authentication](#8-dev-portal-part-two-authentication) - [8.1. Enable Basic Auth](#81-enable-basic-auth) - [8.2. Limit access](#82-limit-access) - [8.2.1. Create a role](#821-create-a-role) - [8.2.2. Add role to Spec](#822-add-role-to-spec) - [8.2.3. Test it !](#823-test-it-) - [8.2.3.1. Register a new developer](#8231-register-a-new-developer) - [8.2.3.2. Approve this developer](#8232-approve-this-developer) - [8.2.3.3. Add role for this developer](#8233-add-role-for-this-developer) - [8.3. Add Oauth2 for developer](#83-add-oauth2-for-developer) - [8.3.1. First, remove basic auth](#831-first-remove-basic-auth) - [8.3.2. Second, add application-registration plugin](#832-second-add-application-registration-plugin) - [8.3.3. Link service and documentation](#833-link-service-and-documentation) - [8.3.3.1. Test it !](#8331-test-it-) - [9. Secure Management Portal](#9-secure-management-portal) - [9.1. Create an admin](#91-create-an-admin) - [9.2. Enable Basic Auth for Kong Manager](#92-enable-basic-auth-for-kong-manager) - [9.3. Use Kong Admin API with RBAC](#93-use-kong-admin-api-with-rbac) - [9.3.1. Create and admin user with a token](#931-create-and-admin-user-with-a-token) - [10. Plugins](#10-plugins) - [10.1. Import a community plugin](#101-import-a-community-plugin) - [10.1.1. Build a new Kong/IAM docker image with the community plugin](#1011-build-a-new-kongiam-docker-image-with-the-community-plugin) - [10.1.2. Test it !](#1012-test-it-) - [10.1.2.1. Use it !](#10121-use-it-) - [10.2. Create a new plugin](#102-create-a-new-plugin) - [10.2.1. File structure](#1021-file-structure) - [10.2.1.1. handler.lua](#10211-handlerlua) - [10.2.1.1.1. Example](#102111-example) - [10.2.1.2. schema.lua](#10212-schemalua) - [10.2.1.3. *.rockspec](#10213-rockspec) - [10.2.2. Build it](#1022-build-it) - [10.2.3. Tips](#1023-tips) - [11. CI/CD](#11-cicd) - [11.1. Create the postman collection](#111-create-the-postman-collection) - [11.1.1. Is IAM startup ?](#1111-is-iam-startup-) - [11.1.2. Delete old datas](#1112-delete-old-datas) - [11.1.3. Create Service/Route](#1113-create-serviceroute) - [11.1.3.1. Tips](#11131-tips) - [11.2. Run it with newman](#112-run-it-with-newman) # 1. Introduction ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/introduction.png "Credit : https://blog.octo.com/kong-le-gorille-de-lapi-management-vu-de-pres/") ## 1.1. What is IAM ? IAM stand for InterSystems API Manager, it's based on **Kong Enterprise Edition**. This mean you have access on top of Kong Open Source edition to : * Manager Portal * Developer Portal * Advance plugin * Oauth2 * Caching * ... ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/KongEEvsOSS.png) ## 1.2. What is an API Management ? API management is the process of creating and publishing web application programming interfaces (APIs), enforcing their usage policies, controlling access, nurturing the subscriber community, collecting and analyzing usage statistics, and reporting on performance. API Management components provide mechanisms and tools to support developer and subscriber community. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/Api_Managment.png) ## 1.3. IAM Portal Kong and IAM are design as API first, this mean, everything done in Kong/IAM can be done by rest calls or the manager portal. During this article all example / exercise will present both this way: |IAM Portal|Rest API| |-----------------|--------------| |![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/default_kong.png)|![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/rest_call.png)| ## 1.4. Flow of this article The aim of this article is to use IAM as a proxy of an IRIS rest API. Definition of this rest API can be found here : ```url http://localhost:52773/swagger-ui/index.html#/ ``` or here ```url https://github.com/grongierisc/iam-training/blob/training/misc/spec.yml ``` Start this article with the main branch. At the end of the article, you should have the same result as the training branch. # 2. Installation ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/installation.png) ## 2.1. What do you need to install? * [Git](https://git-scm.com/downloads) * [Docker](https://www.docker.com/products/docker-desktop) (if you are using Windows, make sure you set your Docker installation to use "Linux containers"). * [Docker Compose](https://docs.docker.com/compose/install/) * [Visual Studio Code](https://code.visualstudio.com/download) + [InterSystems ObjectScript VSCode Extension](https://marketplace.visualstudio.com/items?itemName=daimor.vscode-objectscript) * InterSystems IRIS IAM enabled license file. * IAM Docker image ## 2.2. How IAM works with IRIS At Kong/IAM start, the container check for the Kong/IAM license with a curl call. The endpoint of this call is a rest API on the IRIS container. FYI : Kong license is embedded in IRIS one. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/IAM_IRIS.png) ## 2.3. Setup Git clone this repository. git clone https://github.com/grongierisc/iam-training Run the initial rest API : docker-compose up Test it : ```url http://localhost:52773/swagger-ui/index.html#/ ``` Login/Password : SuperUser/SYS ## 2.4. Install IAM ### 2.4.1. Iris Image First you need to switch for the community edition to a licensed one. To do so, you need to setup your access to InterSystems Container Registry to download IRIS limited access images. Have a look at this [Introducing InterSystems Container Registry](https://community.intersystems.com/post/introducing-intersystems-container-registry) on [Developer Community](https://community.intersystems.com). * Log-in into https://containers.intersystems.com/ using your WRC credentials and get a *token*. * Set up docker login in your computer: ```bash docker login -u="user" -p="token" containers.intersystems.com ``` * Get InterSystems IRIS image: ```bash docker pull containers.intersystems.com/intersystems/irishealth:2020.4.0.524.0 ``` ### 2.4.2. IAM Image In [WRC Software Distribution](https://wrc.intersystems.com/wrc/coDistribution.csp): * Components > Download *IAM-1.5.0.9-4.tar.gz* file, unzip & untar and then load the image: ```bash docker load -i iam_image.tar ``` ### 2.4.3. Update the docker file Change IRIS community edition to a licensed one. * containers.intersystems.com/intersystems/irishealth:2020.4.0.524.0 * add iris.key in key folder Edit the dockerfile to add on top of it this part ```dockerfile ARG IMAGE=containers.intersystems.com/intersystems/irishealth:2020.4.0.524.0 # Frist stage FROM $IMAGE as iris-iam COPY key/iris.key /usr/irissys/mgr/iris.key COPY iris-iam.script /tmp/iris-iam.script RUN iris start IRIS \ && iris session IRIS < /tmp/iris-iam.script \ && iris stop IRIS quietly # Second stage FROM iris-iam ``` This part will create a multi-stage dockerfile. * the first stage is to enable IRIS to serve IAM license. * the second stage is for the REST API build Create a new file iris-iam.script to build a new IRIS Image to enable IAM endpoint and user. ```objectscript zn "%SYS" write "Create web application ...",! set webName = "/api/iam" set webProperties("Enabled") = 1 set status = ##class(Security.Applications).Modify(webName, .webProperties) write:'status $system.Status.DisplayError(status) write "Web application "_webName_" was updated!",! set userProperties("Enabled") = 1 set userName = "IAM" Do ##class(Security.Users).Modify(userName,.userProperties) write "User "_userName_" was updated!",! halt ``` ### 2.4.4. Update the docker-compose Update the docker-compose file to : * db * postgres database for IAM * iam-migration * bootstrap the database * iam * actual IAM instance * a volume for data persistent Add this part to the end of the docker-compose file. ```yml iam-migrations: image: intersystems/iam:1.5.0.9-4 command: kong migrations bootstrap up depends_on: - db environment: KONG_DATABASE: postgres KONG_PG_DATABASE: ${KONG_PG_DATABASE:-iam} KONG_PG_HOST: db KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-iam} KONG_PG_USER: ${KONG_PG_USER:-iam} KONG_CASSANDRA_CONTACT_POINTS: db KONG_PLUGINS: bundled,jwt-crafter ISC_IRIS_URL: IAM:${IRIS_PASSWORD}@iris:52773/api/iam/license restart: on-failure links: - db:db iam: image: intersystems/iam:1.5.0.9-4 depends_on: - db environment: KONG_ADMIN_ACCESS_LOG: /dev/stdout KONG_ADMIN_ERROR_LOG: /dev/stderr KONG_ADMIN_LISTEN: '0.0.0.0:8001' KONG_ANONYMOUS_REPORTS: 'off' KONG_CASSANDRA_CONTACT_POINTS: db KONG_DATABASE: postgres KONG_PG_DATABASE: ${KONG_PG_DATABASE:-iam} KONG_PG_HOST: db KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-iam} KONG_PG_USER: ${KONG_PG_USER:-iam} KONG_PROXY_ACCESS_LOG: /dev/stdout KONG_PROXY_ERROR_LOG: /dev/stderr KONG_PORTAL: 'on' KONG_PORTAL_GUI_PROTOCOL: http KONG_PORTAL_GUI_HOST: '127.0.0.1:8003' KONG_ADMIN_GUI_URL: http://localhost:8002 KONG_PLUGINS: bundled ISC_IRIS_URL: IAM:${IRIS_PASSWORD}@iris:52773/api/iam/license volumes: - ./iam:/iam links: - db:db ports: - target: 8000 published: 8000 protocol: tcp - target: 8001 published: 8001 protocol: tcp - target: 8002 published: 8002 protocol: tcp - target: 8003 published: 8003 protocol: tcp - target: 8004 published: 8004 protocol: tcp - target: 8443 published: 8443 protocol: tcp - target: 8444 published: 8444 protocol: tcp - target: 8445 published: 8445 protocol: tcp restart: on-failure db: image: postgres:9.6 environment: POSTGRES_DB: ${KONG_PG_DATABASE:-iam} POSTGRES_PASSWORD: ${KONG_PG_PASSWORD:-iam} POSTGRES_USER: ${KONG_PG_USER:-iam} volumes: - 'pgdata:/var/lib/postgresql/data' healthcheck: test: ["CMD", "pg_isready", "-U", "${KONG_PG_USER:-iam}"] interval: 30s timeout: 30s retries: 3 restart: on-failure stdin_open: true tty: true volumes: pgdata: ``` Add the .env file in root folder : ```env IRIS_PASSWORD=SYS ``` BTW : Here are the definition of Kong ports : |Port|Protocol|Description| |----|--------|-----------| |:8000|HTTP|Takes incoming HTTP traffic from Consumers, and forwards it to upstream Services.| |:8443|HTTPS|Takes incoming HTTPS traffic from Consumers, and forwards it to upstream Services.| |:8001|HTTP|Admin API. Listens for calls from the command line over HTTP.| |:8444|HTTPS|Admin API. Listens for calls from the command line over HTTPS.| |:8002|HTTP|Kong Manager (GUI). Listens for HTTP traffic.| |:8445|HTTPS|Kong Manager (GUI). Listens for HTTPS traffic.| |:8003|HTTP|Dev Portal. Listens for HTTP traffic, assuming Dev Portal is enabled.| |:8446|HTTPS|Dev Portal. Listens for HTTPS traffic, assuming Dev Portal is enabled.| |:8004|HTTP|Dev Portal /files traffic over HTTP, assuming the Dev Portal is enabled.| |:8447|HTTPS|Dev Portal /files traffic over HTTPS, assuming the Dev Portal is enabled.| ### 2.4.5. Option : add IRIS_PASSWARD as .env For ease of use (and may be security), you can use the .env file in the IRIS dockerfile. To do so, edit the docker-compose with this in the iris service part : ```yml build: context: . dockerfile: dockerfile args: - IRIS_PASSWORD=${IRIS_PASSWORD} ``` And the dockerfile (second or first stage of the build): ```dockerfile ARG IRIS_PASSWORD RUN echo "${IRIS_PASSWORD}" > /tmp/password.txt && /usr/irissys/dev/Container/changePassword.sh /tmp/password.txt ``` ### 2.4.6. Test it ! docker-compose -f "docker-compose.yml" up -d --build # 3. First Service/Route ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/service_route.png) Remember how Kong/IAM works ? ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/KongEEvsOSS.png) Here, we will build : * a service * for our crud API * a route * to access this service ## 3.1. Create a service IAM Portal Rest API # Create service curl -i -X POST \ --url http://localhost:8001/services/ \ --data 'name=crud' \ --data 'url=http://iris:52773/crud/' What do we see here, to create a service we simply need it's url. ## 3.2. Create a route IAM Portal Rest API # Create route curl -i -X POST \ --url http://localhost:8001/services/crud/routes \ --data 'name=crud-route' \ --data 'paths=/persons/*' \ --data 'strip_path=false' What do we see here, to create a route we need : * it's service name * a path where RegEx is allowed ## 3.3. Test it ! **Original API** # Legacy curl –i --location --request GET 'http://localhost:52773/crud/persons/all' \ --header 'Authorization: Basic U3VwZXJVc2VyOlNZUw==' **Proxy API** # KONG curl –i --location --request GET 'http://localhost:8000/persons/all' \ --header 'Authorization: Basic U3VwZXJVc2VyOlNZUw==' What do we see here : * Nothing new on legacy side. * On kong side : * We change the port * The path corresponds to the route * We still need to authenticate # 4. Second, go further with plugin To go further, we will try to auto-authenticate Kong to the IRIS endpoint. To do so, we will use and plugin, resquest-transformer. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/auto_authenticate.png) ## 4.1. Add a plugin to the service IAM Portal Rest API # Create plugin curl -i -X POST \ --url http://localhost:8001/services/crud/plugins \ --data 'name=request-transformer' \ --data 'config.add.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw==' \ --data 'config.replace.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw==' ## 4.2. Test it ! # Legacy **Original API** curl –i --location --request GET 'http://localhost:52773/crud/persons/all' **Proxy API** # KONG curl –i --location --request GET 'http://localhost:8000/persons/all' What do we see here : * Error 401 on the original API * We reach the data without authentication # 5. Third, add our own authentication What we want to achieved here is to add our own authentication without any distuption of the original API. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/custom_auth.png) ## 5.1. Add consumers IAM Portal Rest API # Add consumer anonymous curl -i -X POST \ --url http://localhost:8001/consumers/ \ --data "username=anonymous" \ --data "custom_id=anonymous" # Add consumer user curl -i -X POST \ --url http://localhost:8001/consumers/ \ --data "username=user" \ --data "custom_id=user" ## 5.2. Add Basic auth plugin IAM Portal Rest API # Enable basic auth for service curl -i -X POST http://localhost:8001/routes/crud-route/plugins \ --data "name=basic-auth" \ --data "config.anonymous=5cc8dee4-066d-492e-b2f8-bd77eb0a4c86" \ --data "config.hide_credentials=false" Where : * config.anonymous = uuid of anonymous consumer ## 5.3. Add ACL Plugin IAM Portal Rest API # Enable ACL curl -i -X POST http://localhost:8001/routes/crud-route/plugins \ --data "name=acl" \ --data "config.whitelist=user" ## 5.4. Configure USER with ACL and credentials IAM Portal Rest API # Add consumer group curl -i -X POST \ --url http://localhost:8001/consumers/user/acls \ --data "group=user" # Add consumer credentials curl -i -X POST http://localhost:8001/consumers/user/basic-auth \ --data "username=user" \ --data "password=user" ## 5.5. Test it ! **Original API** # Legacy curl –i --location --request GET 'http://localhost:52773/crud/persons/all' \ --header 'Authorization:Basic dXNlcjp1c2Vy' **Proxy API ** # KONG curl –i --location --request GET 'http://localhost:8000/persons/all' \ --header 'Authorization:Basic dXNlcjp1c2Vy' # 6. Exercice, Rate-Limiting 1. Enable Unauthenticated user 2. Limit rate by 2 calls per minutes to Unauthenticated user ## 6.1. Solution 1. Enable Unauthenticated user IAM Portal Rest API # Add consumer group curl -i -X POST \ --url http://localhost:8001/consumers/anonymous/acls \ --data "group=user" 2. Limit rate by 2 calls per minutes to Unauthenticated user IAM Portal Rest API # Add rate limit consumer curl -i -X POST \ --url http://localhost:8001/consumers/anonymous/plugins \ --data "name=rate-limiting" \ --data "config.limit_by=consumer" \ --data "config.minute=2" # 7. Dev Portal ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/dev_portal_intro.png) ## 7.1. Overview The Kong Developer Portal provides : * a single source of truth for all developers * intuitive content management for documentation * streamlined developer onboarding * role-based access control (RBAC) ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/dev_portal.png) ## 7.2. Enable it ! IAM Portal Rest API curl -X PATCH http://localhost:8001/workspaces/default --data "config.portal=true" ## 7.3. Add your first spec **IAM Portal** **Rest API** curl -X POST http://localhost:8001/default/files -F "path=specs/iam-training.yml" -F "contents=@misc/spec.yml" ## 7.4. Test it ! ```url http://localhost:8003/default/documentation/iam-training ``` What happen ? How-to solve it ? ## 7.5. Exercise 1. Add CORS plugin on route ### 7.5.1. Solution IAM Portal Rest API # Enable CORS curl -i -X POST http://localhost:8001/routes/crud-route/plugins \ --data "name=cors" # 8. Dev Portal, Part two, Authentication ## 8.1. Enable Basic Auth IAM Portal Session Config (JSON) { "cookie_secure": false, "cookie_name": "portal_session", "secret": "SYS", "storage": "kong" } Now authentication is enabled for the dev portal. ## 8.2. Limit access By default, all is accessible for unauthenticated user. We can create role to limit some access. For example, let restrict access to our CRUD API documentation. ### 8.2.1. Create a role IAM Portal Rest API # Enable role curl -i -X POST http://localhost:8001/default/developers/roles \ --data "name=dev" ### 8.2.2. Add role to Spec **IAM Portal** **Rest API** # Enable role curl 'http://localhost:8001/default/files/specs/iam-training.yml' -X PATCH -H 'Accept: application/json, text/plain, */*' --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Origin: http://localhost:8002' -H 'Referer: http://localhost:8002/default/portal/permissions/' --data-raw $'{"contents":"x-headmatter:\\n readable_by:\\n - dev\\nswagger: \'2.0\'\\ninfo:\\n title: InterSystems IRIS REST CRUD demo\\n description: Demo of a simple rest API on IRIS\\n version: \'0.1\'\\n contact:\\n email: apiteam@swagger.io\\n license:\\n name: Apache 2.0\\n url: \'http://www.apache.org/licenses/LICENSE-2.0.html\'\\nhost: \'localhost:8000\'\\nbasePath: /\\nschemes:\\n - http\\nsecurityDefinitions:\\n basicAuth:\\n type: basic\\nsecurity:\\n - basicAuth: []\\npaths:\\n /:\\n get:\\n description: \' PersonsREST general information \'\\n summary: \' Server Info \'\\n operationId: GetInfo\\n x-ISC_CORS: true\\n x-ISC_ServiceMethod: GetInfo\\n responses:\\n \'200\':\\n description: (Expected Result)\\n schema:\\n type: object\\n properties:\\n version:\\n type: string\\n default:\\n description: (Unexpected Error)\\n /persons/all:\\n get:\\n description: \' Retreive all the records of Sample.Person \'\\n summary: \' Get all records of Person class \'\\n operationId: GetAllPersons\\n x-ISC_ServiceMethod: GetAllPersons\\n responses:\\n \'200\':\\n description: (Expected Result)\\n schema:\\n type: array\\n items:\\n $ref: \'#/definitions/Person\'\\n default:\\n description: (Unexpected Error)\\n \'/persons/{id}\':\\n get:\\n description: \' Return one record fo Sample.Person \'\\n summary: \' GET method to return JSON for a given person id\'\\n operationId: GetPerson\\n x-ISC_ServiceMethod: GetPerson\\n parameters:\\n - name: id\\n in: path\\n required: true\\n type: string\\n responses:\\n \'200\':\\n description: (Expected Result)\\n schema:\\n $ref: \'#/definitions/Person\'\\n default:\\n description: (Unexpected Error)\\n put:\\n description: \' Update a record in Sample.Person with id \'\\n summary: \' Update a person with id\'\\n operationId: UpdatePerson\\n x-ISC_ServiceMethod: UpdatePerson\\n parameters:\\n - name: id\\n in: path\\n required: true\\n type: string\\n - name: payloadBody\\n in: body\\n description: Request body contents\\n required: false\\n schema:\\n type: string\\n responses:\\n \'200\':\\n description: (Expected Result)\\n default:\\n description: (Unexpected Error)\\n delete:\\n description: \' Delete a record with id in Sample.Person \'\\n summary: \' Delete a person with id\'\\n operationId: DeletePerson\\n x-ISC_ServiceMethod: DeletePerson\\n parameters:\\n - name: id\\n in: path\\n required: true\\n type: string\\n responses:\\n \'200\':\\n description: (Expected Result)\\n default:\\n description: (Unexpected Error)\\n /persons/:\\n post:\\n description: \' Creates a new Sample.Person record \'\\n summary: \' Create a person\'\\n operationId: CreatePerson\\n x-ISC_ServiceMethod: CreatePerson\\n parameters:\\n - name: payloadBody\\n in: body\\n description: Request body contents\\n required: false\\n schema:\\n type: string\\n responses:\\n \'200\':\\n description: (Expected Result)\\n default:\\n description: (Unexpected Error)\\ndefinitions:\\n Person:\\n type: object\\n properties:\\n Name:\\n type: string\\n Title:\\n type: string\\n Company:\\n type: string\\n Phone:\\n type: string\\n DOB:\\n type: string\\n format: date-time\\n"}' What's important here is this part : ```yml x-headmatter: readable_by: - dev ``` Refere to this documentation : [readable_by attribute](https://docs.konghq.com/enterprise/1.5.x/developer-portal/administration/developer-permissions/#readable_by-attribute) ### 8.2.3. Test it ! #### 8.2.3.1. Register a new developer ![video](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/video/new_dev_sign.gif) #### 8.2.3.2. Approve this developer ![video](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/video/approve_new_dev.gif) #### 8.2.3.3. Add role for this developer ![video](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/video/add_role_dev.gif) curl 'http://localhost:8001/default/developers/dev@dev.com' -X PATCH --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Cache-Control: no-cache' -H 'Origin: http://localhost:8002' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:8002/default/portal/permissions/dev/update' -H 'Pragma: no-cache' --data-raw '{"roles":["dev"]}' ## 8.3. Add Oauth2 for developer In this part we will add an Oauth2 authentication for developers to use securely our crud API. This flow will provide self-registration from developer and grant them access to the crud API. ### 8.3.1. First, remove basic auth To do so, we will replace our basic auth to a bearToken one. First disable our basic auth/acl. **IAM Portal** **Rest API** # Disable ACL Plugin curl 'http://localhost:8001/default/routes/afefe836-b9be-49a8-927a-1324a8597a9c/plugins/3f2e605e-9cb6-454a-83ec-d1b1929b1d30' -X PATCH --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Cache-Control: no-cache' -H 'Origin: http://localhost:8002' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:8002/default/plugins/acl/3f2e605e-9cb6-454a-83ec-d1b1929b1d30/update' -H 'Pragma: no-cache' --data-raw '{"enabled":false,"name":"acl","route":{"id":"afefe836-b9be-49a8-927a-1324a8597a9c"},"config":{"hide_groups_header":false,"whitelist":["user","dev","crud"]}}' ### 8.3.2. Second, add application-registration plugin IAM Portal Rest API # Create application-registration plugin curl -i -X POST \ --url http://localhost:8001/services/crud/plugins \ --data 'name=application-registration' \ --data 'config.auth_header_name=authorization' \ --data 'config.auto_approve=true' \ --data 'config.display_name=auth' \ --data 'config.enable_client_credentials=true' ### 8.3.3. Link service and documentation **IAM Porta** **Rest API** curl 'http://localhost:8001/default/document_objects' --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'Cache-Control: no-cache' -H 'Origin: http://localhost:8002' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:8002/default/services/create-documentation' -H 'Pragma: no-cache' --data-raw '{"service":{"id":"7bcef2e6-117c-487a-aab2-c7e57a0bf61a"},"path":"specs/iam-training.yml"}' #### 8.3.3.1. Test it ! From the dev portal logged as dev@dev.com, create a new application. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/video/add_app_dev.gif) This will give you client_id and client_secret. Theses can be used in the swagger dev portal. Register this application to the crud service : ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/video/register_app_dev.gif) Get token: curl --insecure -X POST https://localhost:8443/persons/oauth2/token \ --data "grant_type=client_credentials" \ --data "client_id=2TXNvDqjeVMHydJbjv9t96lWTXOKAtU8" \ --data "client_secret=V6Vma6AtIvl04UYssz6gAxPc92eCF4KR" Use token : curl --insecure -X GET https://localhost:8443/persons/all \ --header "authorization: Bearer u5guWaYR3BjZ1KdwuBSC6C7udCYxj5vK" # 9. Secure Management Portal ## 9.1. Create an admin As we have bootstrap Kong without a seed password. We have to create an admin before enforcing RBAC. To do so: * Go to Teams * Invite admin * Set Mail * Set Username * Set role to super admin * Invite * Go to Invited Admin * View * Generate link ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/video/invite_admin.gif) ## 9.2. Enable Basic Auth for Kong Manager To enable this feature, we have to change the docker-compose file. Add this to the iam service, environment ```yml KONG_ENFORCE_RBAC: 'on' KONG_ADMIN_GUI_AUTH: 'basic-auth' KONG_ADMIN_GUI_SESSION_CONF: '{"secret":"${IRIS_PASSWORD}","storage":"kong","cookie_secure":false}' ``` Restart the container docker-compose down && docker-compose up -d Go to the invited admin link : ```url http://localhost:8002/register?email=test.test%40gmail.com&username=admin&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTYzMzYzNzEsImlkIjoiY2JiZGE5Y2UtODQ3NS00MmM2LTk4ZjItNDgwZTI4MjQ4NWNkIn0.sFeOc_5UPIr3MdlQrgyGvmvIjRFvSn3nQjo2ph8GrJA ``` ## 9.3. Use Kong Admin API with RBAC As RBAC is set, we can't use kong admin api anymore : curl -s -X GET \ --url http://localhost:8001/routes Get this error : ```json {"message":"Invalid credentials. Token or User credentials required"} ``` ### 9.3.1. Create and admin user with a token * Go to Teams * RBAC Users * Add new user ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/user_rbac.png) curl -s -X GET \ --url http://localhost:8001/routes \ --header "Kong-Admin-Token: SYS" # 10. Plugins ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/plugins_intro.png) Kong come with high quality plugins. But, what if, we need plugin that are not embedded. If we want community plugins ? In this chapiter, we will talk about community plugins, how to import them. Then, we will see how-to build our own plugin. ## 10.1. Import a community plugin For this part, we will be using the jwt-crafter plugin. This plugin adds the possibility to generate a JWT token within Kong itself, eliminating the need for an upstream service doing the token generation. Here is the plugin : ``` https://github.com/grongierisc/kong-plugin-jwt-crafter ``` To install this plugin, as we are using the docker version, we have to build a new image who embed the plugin. ### 10.1.1. Build a new Kong/IAM docker image with the community plugin 1. Create a folder named iam at root of this git. 2. Create a dockerfile in this new folder 3. Create a folder named plugins 1. This is where we will add all our community plugins 4. Update the docker-compose file to enable the new plug in In the plugins folder, git clone our community plugin. git clone https://github.com/grongierisc/kong-plugin-jwt-crafter The dockerfile should look like this: ```dockerfile FROM intersystems/iam:1.5.0.9-4 USER root COPY ./plugins /custom/plugins RUN cd /custom/plugins/kong-plugin-jwt-crafter && luarocks make USER kong ``` What we see in this dockerfile ? Simply to install a community plugin, we have to move to its root folder (where the rockspec is) and call luarocks make. That's it. You have installed the plugin. For the docker-compose part : 1. Edit the iam iamge tag 1. intersystems/iam:1.5.0.9-4 -> intersystems/iam-custom:1.5.0.9-4 2. Add a build context ```yml build: context: iam dockerfile: dockerfile ``` 3. Enable the plugin in the environment variables ```yml KONG_PLUGINS: 'bundled,jwt-crafter' ``` Now build our new iam image : docker-compose build iam ### 10.1.2. Test it ! docker-compose up -d If you go to plugin -> new, at the bottom of the list you should see the jwt-crafter plugin. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/jwt_crafter.png) #### 10.1.2.1. Use it ! 1. Create a new service : IAM Portal Rest API # Create service curl -i -X POST \ --url http://localhost:8001/services/ \ --data 'name=crud-persons' \ --data 'url=http://iris:52773/crud/persons/' 2. Create a route IAM Portal Rest API # Create route curl -i -X POST \ --url http://localhost:8001/services/crud-persons/routes \ --data 'name=crud-route-jwt' \ --data 'paths=/crud/persons/*' \ --data 'strip_path=true' 3. Re-use our auto-auth IAM Portal Rest API # Create plugin curl -i -X POST \ --url http://localhost:8001/services/crud-persons/plugins \ --data 'name=request-transformer' \ --data 'config.add.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw==' \ --data 'config.replace.headers=Authorization:Basic U3VwZXJVc2VyOlNZUw==' Now we are set. The real use of jwt-crafter. ```bash # Add acl to route curl -i -X POST http://localhost:8001/routes/crud-route-jwt/plugins \ --data "name=acl" \ --data "config.whitelist=test" \ --data "config.hide_groups_header=false" # Create service curl -i -X POST \ --url http://localhost:8001/services/ \ --data 'name=jwt-login' \ --data 'url=http://neverinvoked/' # Create route curl -i -X POST \ --url http://localhost:8001/services/jwt-login/routes \ --data 'name=jwt-login-route' \ --data 'paths=/jwt/log-in' # Enable basic auth for service curl -i -X POST http://localhost:8001/routes/jwt-login-route/plugins \ --data "name=basic-auth" \ --data "config.hide_credentials=false" # Enable basic auth for service curl -i -X POST http://localhost:8001/routes/jwt-login-route/plugins \ --data "name=jwt-crafter" \ --data "config.expires_in=86400" # Add consumer curl -i -X POST \ --url http://localhost:8001/consumers/ \ --data "username=test" # Add consumer group curl -i -X POST \ --url http://localhost:8001/consumers/test/acls \ --data "group=test" # Add consumer credentials curl -i -X POST http://localhost:8001/consumers/test/basic-auth \ --data "username=test" \ --data "password=test" curl -i -X POST http://localhost:8001/consumers/test/jwt \ --data "key=test" \ --data "algorithm=HS256" # JWT plugins curl -i -X POST http://localhost:8001/routes/crud-route-jwt/plugins \ --data "name=jwt" ``` Test it ! ```bash # test:test is base64 encoded curl -H 'Authorization: basic dGVzdDp0ZXN0' localhost:8000/jwt/log-in ``` ```bash curl --location --request GET 'http://localhost:8000/crud/persons/all' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW0iOiJ0ZXN0Iiwic3ViIjoiODJiNjcwZDgtNmY2OC00NDE5LWJiMmMtMmYxZjMxNTViN2E2Iiwicm9sIjpbInRlc3QiXSwiZXhwIjoxNjE2MjUyMTIwLCJpc3MiOiJ0ZXN0In0.g2jFqe0hDPumy8_gG7J3nYsuZ8KUz9SgZOecdBDhfns' ``` ## 10.2. Create a new plugin This is not the place to learn lua. But I'll give you some tips like how to quickly restart IAM to test our new development. ### 10.2.1. File structure ``` kong-plugin-helloworld ├── kong │ └── plugins │ └── helloworld │ ├── handler.lua │ └── schema.lua └── kong-plugin-helloworld-0.1.0-1.rockspec ``` By convention, kong plugins must be prefix by kong-plugin. In our example, the name of the plugin is helloworld. Three files are mandatory : * handler.lua: the core of your plugin. It is an interface to implement, in which each function will be run at the desired moment in the lifecycle of a request / connection. * schema.lua: your plugin probably has to retain some configuration entered by the user. This module holds the schema of that configuration and defines rules on it, so that the user can only enter valid configuration values. * *.rockspec: Rockspec: a package specification file A declarative Lua script, with rules on how to build and package rocks *.rockspec - a Lua file containing some tables. #### 10.2.1.1. handler.lua The plugins interface allows you to override any of the following methods in your handler.lua file to implement custom logic at various entry-points of the execution life-cycle of Kong: |Function name |Phase |Description| |----|----|----| |:init_worker() |init_worker |Executed upon every Nginx worker process’s startup.| |:certificate() |ssl_certificate |Executed during the SSL certificate serving phase of the SSL handshake.| |:rewrite() |rewrite |Executed for every request upon its reception from a client as a rewrite phase handler. NOTE in this phase neither the Service nor the Consumer have been identified, hence this handler will only be executed if the plugin was configured as a global plugin!| |:access() |access |Executed for every request from a client and before it is being proxied to the upstream service.| |:response() |access |Replaces both header_filter() and body_filter(). Executed after the whole response has been received from the upstream service, but before sending any part of it to the client.| |:header_filter() |header_filter |Executed when all response headers bytes have been received from the upstream service.| |:body_filter() |body_filter |Executed for each chunk of the response body received from the upstream service. Since the response is streamed back to the client, it can exceed the buffer size and be streamed chunk by chunk. hence this method can be called multiple times if the response is large. See the lua-nginx-module documentation for more details.| |:log() |log |Executed when the last response byte has been sent to the client.| ##### 10.2.1.1.1. Example ```lua local BasePlugin = require "kong.plugins.base_plugin" local HelloWorldHandler = BasePlugin:extend() function HelloWorldHandler:new() HelloWorldHandler.super.new(self, "helloworld") end function HelloWorldHandler:access(conf) HelloWorldHandler.super.access(self) if conf.say_hello then ngx.log(ngx.ERR, "============ Hello World! ============") ngx.header["Hello-World"] = "Hello World!!!" else ngx.log(ngx.ERR, "============ Bye World! ============") ngx.header["Hello-World"] = "Bye World!!!" end end return HelloWorldHandler ``` #### 10.2.1.2. schema.lua Simply the configuration file see in the portal. ```lua return { no_consumer = true, fields = { say_hello = { type = "boolean", default = true }, say_hello_body = { type = "boolean", default = true } } } ``` #### 10.2.1.3. *.rockspec ```rockspec package = "kong-plugin-helloworld" -- hint: rename, must match the info in the filename of this rockspec! -- as a convention; stick to the prefix: `kong-plugin-` version = "0.1.0-1" -- hint: renumber, must match the info in the filename of this rockspec! -- The version '0.1.0' is the source code version, the trailing '1' is the version of this rockspec. -- whenever the source version changes, the rockspec should be reset to 1. The rockspec version is only -- updated (incremented) when this file changes, but the source remains the same. -- TODO: This is the name to set in the Kong configuration `plugins` setting. -- Here we extract it from the package name. local pluginName = package:match("^kong%-plugin%-(.+)$") -- "myPlugin" supported_platforms = {"linux", "macosx"} source = { url = "https://github.com/grongierisc/iam-training", branch = "master", -- tag = "0.1.0" -- hint: "tag" could be used to match tag in the repository } description = { summary = "This a demo helloworld for Kong plugin", homepage = "https://github.com/grongierisc/iam-training", license = "Apache 2.0" } dependencies = { "lua >= 5.1" -- other dependencies should appear here } build = { type = "builtin", modules = { ["kong.plugins."..pluginName..".handler"] = "kong/plugins/"..pluginName.."/handler.lua", ["kong.plugins."..pluginName..".schema"] = "kong/plugins/"..pluginName.."/schema.lua", } } ``` ### 10.2.2. Build it We will be doing the same as here : [11.1.1. Build a new Kong/IAM docker image with the community plugin](#1111-build-a-new-kongiam-docker-image-with-the-community-plugin) But adapted to our plugin : Dockerfile : ```dockerfile FROM intersystems/iam:1.5.0.9-4 USER root COPY ./plugins /custom/plugins RUN cd /custom/plugins/kong-plugin-jwt-crafter && luarocks make RUN cd /custom/plugins/kong-plugin-helloworld && luarocks make #USER kong #Stay with root use, we will see why later ``` Enable the plugin in the environment variables ```yml KONG_PLUGINS: 'bundled,jwt-crafter,helloworld' ``` Now build our new iam image : docker-compose build iam Then docker-compose up and test it. ### 10.2.3. Tips To run the IAM container in "debug mode", to easily stop/restart it, modify the dockerfile to add/remove plugin and so on. You can stop iam service : docker-compose stop iam And start it in run mode with a shell : docker-compose run -p 8000:8000 -p 8001:8001 -p 8002:8002 iam sh In the container : ./docker-entrypoint.sh kong Happy coding :) # 11. CI/CD ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/cd_ci_intro.png) We are close to the end of this article. To finish let's talk about DevOps/CI/CD. The aim of this chapter is to give you some ideas about how to implement/script ci/cd for IAM/Kong. As Kong is API first, the idea is to script all the rest calls and play then on each environment. The easiest way to script rest calls is with postman and his best friend newman (command line version of postman). ## 11.1. Create the postman collection One thing handy with postman is its ability to run script before and after a rest call. We will use this functionality in most cases. ### 11.1.1. Is IAM startup ? Our first script will check if IAM is up and running. ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/postman_startup.png) ```javascript var iam_url = pm.environment.get("iam_url"); var iam_config_port = pm.environment.get("iam_config_port"); var url = "http://" + iam_url + ":" + iam_config_port + "/"; SenReq(20); async function SenReq(maxRequest) { var next_request = "end request"; const result = await SendRequest(maxRequest); console.log("result:",result); if(result == -1) { console.error("IAM starting .... failed !!!!"); } } function SendRequest(maxRequest) { return new Promise(resolve => { pm.sendRequest(url, function (err) { if (err) { if (maxRequest > 1) { setTimeout(function () {}, 5000); console.warn("IAM not started...retry..next retry in 5 sec"); SendRequest(maxRequest - 1); } else { console.error("IAM starting .... failed"); resolve(-1); } } else { console.log("IAM starting .... ok"); resolve(1); } } ); }); } ``` ### 11.1.2. Delete old datas ```javascript var iam_url=pm.environment.get("iam_url"); var iam_config_port=pm.environment.get("iam_config_port"); pm.sendRequest("http://"+iam_url+":"+iam_config_port+"/plugins", function (err, res) { if (err) { console.log("ERROR : ",err); } else { var body_json=res.json(); if(body_json.data) { for( i=0; i < body_json.data.length; i++) { // Example with a full fledged SDK Request route_id = body_json.data[i].id; const delete_route = { url: "http://"+iam_url+":"+iam_config_port+"/plugins/" + route_id, method: 'DELETE', }; pm.sendRequest(delete_route, function(err, res){ console.log(err ? err : res); }); } } } }); ``` Do the same for routes, services and consumers. This order is important beause you can't remove services with routes. ### 11.1.3. Create Service/Route Routes are dependent from services. For this type of cases we can use Test function of postman to retrieve data : ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/postman_form_data.png) Screen Script var id = pm.response.json().id; var name = pm.response.json().name; pm.globals.set("service_crud_id", id); pm.globals.set("service_crud_name", name); Here we save from the response the id and name of the new services. Then we can use it in the next route creation : Screen Script service_crud_name = pm.globals.get("service_crud_name"); Here we retrieve the global variable "service_crud_name". Then, use it in the actual call. Screen Script { "paths": [ "/persons/*" ], "protocols": [ "http" ], "name": "crud-persons", "strip_path": false, "service": { "name": "{{service_crud_name}}" } } #### 11.1.3.1. Tips * paylaod can be either json or form-data * form-data : ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/postman_form_data.png) * json : ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/postman_json.png) Easy way to get the json format, go to the manager portal, view, copy json : ![alt](https://raw.githubusercontent.com/grongierisc/iam-training/training/misc/img/postman_get_json.png) ## 11.2. Run it with newman docker run --rm -v "`pwd`/ci/":"/etc/newman" \ --network="iam-training_default" \ -t postman/newman run "DevOps_IAM.postman_collection.json" \ --environment="DevOps_IAM.postman_environment.json"
Announcement
Evgeny Shvarov · Mar 26, 2021

Bonus Points for InterSystems Developer Tools Contest

Hi Developers! Here're the technology bonuses for the InterSystems Developer Tools Contest that will give you extra points in the voting. Docker container usage - 2 points The application gets a 'Docker container' bonus if it uses InterSystems IRIS running in a docker container. Here is the simplest template to start from. ZPM Package deployment - 2 points You can collect the bonus if you build and publish the ZPM(ObjectScript Package Manager) package for your Full-Stack application so it could be deployed with: zpm "install your-multi-model-solution" command on IRIS with ZPM client installed. ZPM client. Documentation. Unit Testing - 2 points Applications that have Unit Testing for the InterSystems IRIS code will collect the bonus. Learn more about ObjectScript Unit Testing in Documentation and on Developer Community. Online Demo of your project - 3 pointsCollect 3 more bonus points if you provision your project to the cloud as an online demo. You can use this template or any other deployment option. Example. Learn more on deployment in Monday's Kick-Off webinar. Code quality analysis with zero bugs - 2 points Include the code quality Github action for code static control and make it show 0 bugs for ObjectScript. Learn more in Monday's Kick-Off webinar. Article on Developer Community - 2 points Post an article on Developer Community that describes features of your project. Collect 2 points for each article. Translations to different languages work too. Video on YouTube - 3 points Make the Youtube video that demonstrates your product in action and collect 3 bonus points per each. Example. The list of bonuses is subject to change. Stay tuned!
Article
Yuri Marx · Jun 22, 2020

Create cognitive digital service with InterSystems IRIS

The Intersystems IRIS is a great platform to develop, run and consume Data Science services. IRIS can ingest data from any type, format, protocol and time using adapters. These datasets can be prepared with BPL, DTL and Object Script and stored as SQL or NoSQL data. Finally, it can be consumed by open ML algorithms inside IRIS and visualized in the IRIS dashboards. See more in: https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=PAGE_data_science.
Announcement
Anastasia Dyubaylo · Oct 5, 2020

InterSystems Full Stack Contest: Voting Time!

Hey Developers! This week is a voting week for the InterSystems Full Stack Contest! So, it's time to give your vote to the best solutions built with InterSystems IRIS. 🔥 You decide: VOTING IS HERE 🔥 How to vote? This is easy: you will have one vote, and your vote goes either in Experts Nomination or in Community Nomination. Experts Nomination If you are InterSystems Product Manager, or DC moderator, or Global Master from Specialist level and above cast your vote in the Expert nomination. Community Nomination If you ever contributed to DC (posts or replies) and this was not considered as spam you are able to vote for the applications in the Community nomination. Voting Voting takes place on the Open Exchange Contest Page and you need to sign in to Open Exchange – you can do it with your DC account credentials. If you changed your mind, cancel the choice and give your vote to another application – you have 7 days to choose! Contest participants are allowed to fix the bugs and make improvements to their applications during the voting week, so don't miss and subscribe to application releases! ➡️ Also, please check out the Judgment and Voting Rules for the Contest here. OK! After the first day of the voting we have: Expert Nomination, Top 3 npm-iris – 3 apptools-admin – 3 qewd-conduit – 3 ➡️ The leaderboard. Community Nomination, Top 3 npm-iris – 9 apptools-admin – 1 qewd-conduit – 1 WebSocket_with_Fhir – 1 ➡️ The leaderboard. Experts, we are waiting for your votes! 🔥 Participants, improve & promote your solutions! Here are the results after 2 days of voting: Expert Nomination, Top 3 npm-iris – 7 apptools-admin – 6 realworld-intersystems-iris – 3 qewd-conduit – 3 ➡️ The leaderboard. Community Nomination, Top 3 npm-iris – 11 apptools-admin – 4 restoreUI – 3 ➡️ The leaderboard. So, the voting continues. Please support the application you like! Hey Developers, Technology bonuses for the contest apps have been published. Check them out and cast your vote for the application you like! ✅ Developers! Only 3 days left before the end of voting. Please check out the Contest Board and vote for the solutions you like! 👍🏼