diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fc5173..ffba79e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.0.4] - 2024-08-18 + +### Added + +- If forms.yaml is missing, it will be auto created. This is useful for new installations without docker-compose +- Same for certificates + +### Changed + +- Async await replacements for promises (readability) + +### Fixed + +- Some credentials bugfixes + ## [5.0.3] - 2024-06-21 ### Added @@ -709,7 +724,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow change password for current local user - Start tracking versions -[Unreleased]: https://github.com/ansibleguy76/ansibleforms/compare/5.0.3...HEAD +[Unreleased]: https://github.com/ansibleguy76/ansibleforms/compare/5.0.4...HEAD + +[5.0.4]: https://github.com/ansibleguy76/ansibleforms/compare/5.0.3...5.0.4 [5.0.3]: https://github.com/ansibleguy76/ansibleforms/compare/5.0.2...5.0.3 diff --git a/app_versions.gradle b/app_versions.gradle index 22ed8bcf..fc4569a3 100644 --- a/app_versions.gradle +++ b/app_versions.gradle @@ -1,2 +1,2 @@ -ext.version_code = 50003 -ext.version_name = "5.0.3" +ext.version_code = 50004 +ext.version_name = "5.0.4" diff --git a/client/package.json b/client/package.json index f61e0c28..41a0a7d9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ansible_forms_vue", - "version": "5.0.3", + "version": "5.0.4", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -19,7 +19,7 @@ "@fortawesome/free-regular-svg-icons": "~6.5.2", "@fortawesome/free-solid-svg-icons": "~6.5.2", "@fortawesome/vue-fontawesome": "2.0.10", - "axios": "~1.7.2", + "axios": "~1.7.4", "brace": "~0.11.1", "bulma": "0.9.4", "bulma-calendar": "6.1.19", @@ -28,7 +28,7 @@ "bulma-quickview": "*", "bulmaswatch": "0.8.1", "copy-to-clipboard": "~3.3.3", - "core-js": "~3.37.1", + "core-js": "~3.38.0", "es6-promise": "~4.2.8", "highlight.js": "9.11.0", "jsonwebtoken": "^9.0.2", diff --git a/client/src/components/Form.vue b/client/src/components/Form.vue index 1f05ba6e..2b13eb19 100644 --- a/client/src/components/Form.vue +++ b/client/src/components/Form.vue @@ -1740,7 +1740,7 @@ var ref=this axios({ method: 'get', - headers, + headers: headers.headers, url, responseType: 'arraybuffer', }) diff --git a/client/src/views/Credentials.vue b/client/src/views/Credentials.vue index 55bf7382..feb11640 100644 --- a/client/src/views/Credentials.vue +++ b/client/src/views/Credentials.vue @@ -151,7 +151,16 @@ }else{ console.log("No item selected") this.credential = { - name:"" + is_database:false, + name:"", + user:"", + password:"", + host:"NA", + port:3306, + db_name:"", + description:"", + secure:false, + db_type:"" } } }, diff --git a/client/src/views/Jobs.vue b/client/src/views/Jobs.vue index 15a17458..55868d69 100644 --- a/client/src/views/Jobs.vue +++ b/client/src/views/Jobs.vue @@ -383,7 +383,7 @@ var ref=this axios({ method: 'get', - headers, + headers: headers.headers, url, responseType: 'arraybuffer', }) diff --git a/client/src/views/Logs.vue b/client/src/views/Logs.vue index b10bbc6e..dfbe5310 100644 --- a/client/src/views/Logs.vue +++ b/client/src/views/Logs.vue @@ -104,7 +104,7 @@ var ref=this axios({ method: 'get', - headers, + headers:headers.headers, url, responseType: 'arraybuffer', }) diff --git a/docs/_data/help.yaml b/docs/_data/help.yaml index bfbb5b23..23b556cf 100644 --- a/docs/_data/help.yaml +++ b/docs/_data/help.yaml @@ -3837,7 +3837,7 @@ 'get', 'https://resturl/api/', '', - {'a_custom_http_header','your_value'}, + {'a_custom_http_header':'your_value'}, '.records[].name', {name:{ignoreCase:true,direction:'desc'}}, false @@ -4217,7 +4217,7 @@ description: "" awxCredentials: - vmware - execution_environment: my_execution_environment + executionEnvironment: my_execution_environment roles: - public categories: [] @@ -4261,7 +4261,7 @@ columns: - name valueColumn: name - - name: __execution_environment__ # use this special name to override the execution_environment from the form + - name: __executionEnvironment__ # use this special name to override the executionEnvironment from the form label: Inventory type: enum expression: "fn.fnRestJwtSecure('get','https://172.16.50.1/api/v2/execution_environments?organization=$(organization)','','awx_rest','[.results[]]')" @@ -4307,11 +4307,11 @@ You can also choose if the repository must be cloned when AnsibleForms starts, and you can add cron-schedule to schedule recurring pull-actions. Additionally, in the swagger interface, you will find a clone and pull rest api for webhooks. In case you want long-lived access tokens for the webhooks, with swagger you can pass an expiryDays parameter (for admin roles only) and create long-lived tokens. - - name: Host Ansibleforms in a subfolder - short: Host Ansibleforms in a subfolder - description: | - In the case you want to host ansibleforms in subfolder, for example `https://af.domain.local/mysubfolder/`. - You can use the environment variable `BASE_URL`. Set it to `/mysubfolder/` (default is `/`). + # - name: Host Ansibleforms in a subfolder + # short: Host Ansibleforms in a subfolder + # description: | + # In the case you want to host ansibleforms in subfolder, for example `https://af.domain.local/mysubfolder/`. + # You can use the environment variable `BASE_URL`. Set it to `/mysubfolder/` (default is `/`). - name: Enable ytt short: Enable ytt description: | diff --git a/docs/installation.md b/docs/installation.md index 4acee819..8b554b54 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -34,9 +34,14 @@ AnsibleForms can be installed in a few ways. -# Install using Docker-Compose +# Install using Docker-Compose + +The recommended way to install AnsibleForms is using `Docker Compose`, which is the fastest way to start AnsibleForms with Docker. However, if you are skilled with docker, podman and/or Kubernetes, the [docker-compose (with Kubernetes sample)](https://github.com/ansibleguy76/ansibleforms-docker), together with the environment variables should get you started as well. + +
Note You can also use Podman and Podman-Compose. The commands are similar (docker- > podman and docker-compose -> podman-compose)
+Note Using docker and docker-compose for the first time, requires some basic linux skills and some knowledge about containers
@@ -45,7 +50,7 @@ The recommended way to install AnsibleForms is using `Docker Compose`, which is ## Prerequisites -* **Linux machine** : Any flavour should do, The need of CPU and memory is not very high, but, of course can grow if you start a lot of playbooks simultaniously +* **Linux machine** : Any flavour should do, The need of CPU and memory is not very high, but, of course can grow if you start a lot of playbooks simultaniously. When using Podman, I recommand Debian (ubuntu has some issues with Podman) * **Github access** : The easiest way is to download or clone the docker-compose project on Github * **Install Docker** : You need to have a container environment, and in this example we use Docker * **Install Docker Compose** : To spin-up AnsibleForms and MySql with docker, using a few simple configuration-files, we need Docker Compose @@ -68,10 +73,8 @@ cd /srv/apps ## Clone the docker-compose project ```bash -# centos -sudo yum install -y git -# ubuntu +# ubuntu or debian sudo apt-get install -y git sudo git init @@ -94,10 +97,7 @@ sudo chmod -R +x ./data/mysql/init/ [Docker installation manuals](https://docs.docker.com/engine/install) ```bash -# centos -yum install -y docker-ce docker-ce-cli containerd.io docker-compose - -# ubuntu +# ubuntu / debian sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-compose # the below is to ensure dns works properly inside the dockerimages @@ -109,6 +109,13 @@ sudo systemctl start docker sudo systemctl enable docker ``` +## Install Podman and podman-compose + +```bash +# ubuntu / debian +sudo apt-get install -y podman podman-compose +``` + ## Customize Feel free to look at the variables in the `.env` file and `docker-compose.yaml` file. @@ -118,6 +125,10 @@ Feel free to look at the variables in the `.env` file and `docker-compose.yaml` ```bash sudo docker-compose up -d +# note, with some plavors and versions, it's `docker compose` (with a space) +# or +sudo podman-compose up -d +# note that podman is service-less. You can run it as any user. Your choice to use sudo or not. ``` ## Test the application diff --git a/docs/introduction.md b/docs/introduction.md index 29291236..d0951877 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -40,13 +40,14 @@ page_nav: * **Swagger API** : Has a rest-api and Swagger documentation * **Job History & Log** : See the history of your jobs* * **Designer** : Although the forms are NOT built using a graphical designer, a YAML based editor/designer with validation is present +* **Git integration** : Sync your forms config files, ansible playbooks and other required files with a git repo # Form Capabilities * **Categories** : Group multiple forms under categories * **Role based access** : Limit forms based on roles * **Cascaded dropdowns** : Allow references between fields to create responsive, cascaded dropdown boxes -* **Database sources** : Import data into fields from databases (MySql, MSSql, Postgres, Mongo) +* **Database sources** : Import data into fields from databases (MySql, MSSql, Postgres, Mongo, Oracle) * **Expression based sources** : Import data using serverside expressions (javascript), such as Rest API's, json-files, yaml-files, ... and filter, manipulate and sort them * **Local expressions** : Use the power of javascript (local browser sandbox) to calculate, manipulate, generate, ... * **Field dependencies** : Show/hide fields based on values of other fields diff --git a/server/config/https.config.js b/server/config/https.config.js index bd3a8ebb..5a5d0ceb 100644 --- a/server/config/https.config.js +++ b/server/config/https.config.js @@ -1,12 +1,43 @@ const fs=require('fs') +const path=require('path') const logger=require("../src/lib/logger"); var privatekey=undefined var certificate=undefined if(process.env.HTTPS=="1"){ + + var certificatePath = process.env.HTTPS_CERT || (__dirname + '/../persistent/certificates/cert.pem') + var privatekeyPath = process.env.HTTPS_KEY || (__dirname + '/../persistent/certificates/key.pem') + // logger.info("Using https certificate : " + certificatePath) + // logger.info("Using https private key : " + privatekeyPath) + + // check if httpsConfig.httpsKey and httpsConfig.httpsCert exist + if(!fs.existsSync(certificatePath) || !fs.existsSync(privatekeyPath)){ + logger.warning("httpsKey or httpsCert not found, copying from templates") + var certificateTemplatePath = path.join(__dirname,"/../templates/cert.pem.template") + var privatekeyTemplatePath = path.join(__dirname,"/../templates/key.pem.template") + var certificateDirPath = path.dirname(certificatePath) + // logger.info("Using https certificate template : " + certificateTemplatePath) + // logger.info("Using https private key template : " + privatekeyTemplatePath) + try{ + // create the folder if it doesn't exist + logger.info("Creating folder " + certificateDirPath) + fs.mkdirSync(certificateDirPath, { recursive: true }); + logger.info("Copying templates to " + certificatePath + " and " + privatekeyPath) + fs.copyFileSync(certificateTemplatePath, certificatePath); + fs.copyFileSync(privatekeyTemplatePath, privatekeyPath); + logger.info("Copied templates to " + certificatePath + " and " + privatekeyPath) + }catch(e){ + logger.error("No certificate found and could not copy templates",e) + // exit // no point to continue + process.exit(1) + } + } + + try{ - privatekey = fs.readFileSync(process.env.HTTPS_KEY || (__dirname + '/../persistent/certificates/key.pem')) - certificate = fs.readFileSync(process.env.HTTPS_CERT || (__dirname + '/../persistent/certificates/cert.pem')) + privatekey = fs.readFileSync(privatekeyPath) + certificate = fs.readFileSync(certificatePath) }catch(err){ logger.error("Failed to open https private key and certificate : ",err) throw new Error("Failed to open https private key and certificate : " + err.message) diff --git a/server/package.json b/server/package.json index 39587f53..cafec81a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "ansible_forms", - "version": "5.0.3", + "version": "5.0.4", "repository": { "type": "git", "url": "git://github.com/ansibleguy76/ansibleforms.git" @@ -20,34 +20,34 @@ "@outlinewiki/passport-azure-ad-oauth2": "~0.1.0", "ajv": "~6.12.6", "ajv-error-parser": "~1.0.7", - "axios": "~1.7.2", + "axios": "~1.7.4", "bcrypt": "~5.1.0", "bluebird": "~3.7.2", "cert-info": "~1.5.1", - "cheerio": "~1.0.0-rc.12", + "cheerio": "~1.0.0", "connect-history-api-fallback": "~2.0.0", - "core-js": "~3.37.1", + "core-js": "~3.38.0", "cors": "~2.8.5", "cron-parser": "~4.9.0", - "dayjs": "1.11.11", + "dayjs": "1.11.12", "express": "~4.19.2", "cookie-session": "~2.1.0", "fs-extra": "~11.2.0", "ip": "2.0.1", "json-bigint": "~1.0.0", - "ldap-authentication": "~3.2.1", + "ldap-authentication": "~3.2.2", "ldapjs": "~3.0.7", "lodash": "~4.17.21", "modern-passport-http": "~0.3.0", "moment": "~2.30.1", - "mongodb": "~6.7.0", - "oracledb": "~6.5.1", + "mongodb": "~6.8.0", + "oracledb": "~6.6.0", "mssql": "~10.0.2", "multer": "~1.4.5-lts.1", - "mysql2": "~3.10.0", + "mysql2": "~3.11.0", "node-cache": "~5.1.2", "node-jq": "~4.4.0", - "nodemailer": "~6.9.8", + "nodemailer": "~6.9.14", "openid-client": "^5.6.5", "passport": "~0.7.0", "passport-jwt": "~4.0.1", @@ -55,16 +55,16 @@ "read-last-lines": "~1.8.0", "swagger-ui-express": "~5.0.1", "thenby": "~1.3.4", - "winston": "~3.13.0", + "winston": "~3.14.1", "winston-daily-rotate-file": "~5.0.0", "winston-syslog": "~2.7.0", "yaml": "~2.4.5" }, "devDependencies": { "@babel/cli": "~7.24.7", - "@babel/core": "7.24.7", - "@babel/eslint-parser": "7.24.7", - "@babel/node": "~7.24.7", + "@babel/core": "7.25.2", + "@babel/eslint-parser": "7.25.1", + "@babel/node": "~7.25.0", "dotenv": "~16.4.1", "eslint": "~8.56.0", "nodemon": "~3.1.3", diff --git a/server/src/controllers/config.controller.js b/server/src/controllers/config.controller.js index 6339ef4b..a4dfa6ba 100644 --- a/server/src/controllers/config.controller.js +++ b/server/src/controllers/config.controller.js @@ -13,6 +13,7 @@ exports.findAll = async function(req,res){ var forms = await Form.load() res.json(forms) }catch(err){ + // console.log(err) res.json({error:helpers.getError(err)}) } } diff --git a/server/src/controllers/group.controller.js b/server/src/controllers/group.controller.js index 2a49b956..e6b598c2 100644 --- a/server/src/controllers/group.controller.js +++ b/server/src/controllers/group.controller.js @@ -51,15 +51,16 @@ exports.update = function(req, res) { .catch((err)=>{res.json(new RestResult("error","failed to update group",null,err.toString()))}) } }; -exports.delete = function(req, res) { - Group.delete( req.params.id) - .then((deleted)=>{ - if(deleted.affectedRows==1){ - res.json(new RestResult("success","group deleted",null,"")) - }else{ - res.json(new RestResult("error","unknown group or group has users",null,`affected rows : ${deleted.affectedRows}`)) - } +exports.delete = async function(req, res) { + try{ + const deleted = await Group.delete( req.params.id) + if(deleted.affectedRows==1){ + res.json(new RestResult("success","group deleted",null,"")) + }else{ + res.json(new RestResult("error","unknown group or group has users",null,`affected rows : ${deleted.affectedRows}`)) + } + }catch(err){ + res.json(new RestResult("error","failed to delete group",null,err.toString())) + } - }) - .catch((err)=>{res.json(new RestResult("error","failed to delete group",null,err.toString()))}) }; diff --git a/server/src/controllers/repository.controller.js b/server/src/controllers/repository.controller.js index 2673f47c..993adc5a 100644 --- a/server/src/controllers/repository.controller.js +++ b/server/src/controllers/repository.controller.js @@ -47,6 +47,11 @@ exports.clone = function(req, res) { .then(()=>{res.json(new RestResult("success","repository cloned",null,""))}) .catch((err)=>{ res.json(new RestResult("error","failed to clone repository",null,err.toString())) }) }; +exports.reset = function(req, res) { + Repository.reset(req.params.name) + .then(()=>{res.json(new RestResult("success","repository reset",null,""))}) + .catch((err)=>{ res.json(new RestResult("error","failed to reset repository",null,err.toString())) }) + }; exports.pull = function(req, res) { Repository.pull(req.params.name) .then(()=>{res.json(new RestResult("success","repository pulled",null,""))}) diff --git a/server/src/db/create_schema_and_tables.sql b/server/src/db/create_schema_and_tables.sql index 7fcda862..6826bab3 100644 --- a/server/src/db/create_schema_and_tables.sql +++ b/server/src/db/create_schema_and_tables.sql @@ -1,6 +1,10 @@ +-- disable foreign key checks to avoid errors when creating tables SET FOREIGN_KEY_CHECKS=0; +-- create the database if it does not exist CREATE DATABASE /*!32312 IF NOT EXISTS*/`AnsibleForms` /*!40100 DEFAULT CHARACTER SET utf8 */; +-- use the database USE `AnsibleForms`; +-- create groups table DROP TABLE IF EXISTS `groups`; CREATE TABLE `groups`( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -8,6 +12,7 @@ CREATE TABLE `groups`( PRIMARY KEY (`id`), UNIQUE KEY `uk_AnsibleForms_groups_natural_key` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +-- create users table DROP TABLE IF EXISTS `users`; CREATE TABLE `users`( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -20,6 +25,7 @@ CREATE TABLE `users`( KEY `FK_users_group` (`group_id`), CONSTRAINT `FK_users_group` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +-- create tokens table DROP TABLE IF EXISTS `tokens`; CREATE TABLE `tokens` ( `username` varchar(250) NOT NULL, @@ -27,6 +33,7 @@ CREATE TABLE `tokens` ( `refresh_token` text DEFAULT NULL, `timestamp` datetime NOT NULL DEFAULT current_timestamp() ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- create credentials table DROP TABLE IF EXISTS `credentials`; CREATE TABLE `credentials` ( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -43,6 +50,7 @@ CREATE TABLE `credentials` ( PRIMARY KEY (`id`), UNIQUE KEY `uk_AnsibleForms_credentials_natural_key` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- create ldap table DROP TABLE IF EXISTS `ldap`; CREATE TABLE `ldap` ( `server` varchar(250) DEFAULT NULL, @@ -64,6 +72,7 @@ CREATE TABLE `ldap` ( `mail_attribute` varchar(250) DEFAULT NULL, `enable` tinyint(4) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- create awx table DROP TABLE IF EXISTS `awx`; CREATE TABLE `awx` ( `uri` varchar(250) NOT NULL, @@ -74,6 +83,7 @@ CREATE TABLE `awx` ( `ignore_certs` tinyint(4) DEFAULT NULL, `ca_bundle` text DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- create job_output and jobs tables DROP TABLE IF EXISTS `job_output`; DROP TABLE IF EXISTS `jobs`; CREATE TABLE `jobs` ( @@ -106,6 +116,7 @@ CREATE TABLE `job_output` ( KEY `FK_job_output_jobs` (`job_id`), CONSTRAINT `FK_job_output_jobs` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=1650 DEFAULT CHARSET=utf8; +-- create settings table DROP TABLE IF EXISTS `settings`; CREATE TABLE `settings` ( `mail_server` varchar(250) DEFAULT NULL, @@ -117,17 +128,50 @@ CREATE TABLE `settings` ( `url` varchar(250) DEFAULT NULL, `forms_yaml` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -USE `AnsibleForms`; +-- create azuread table DROP TABLE IF EXISTS `azuread`; CREATE TABLE `azuread` ( `client_id` text DEFAULT NULL, `secret_id` text DEFAULT NULL, `enable` tinyint(4) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- create oidc table +DROP TABLE IF EXISTS `oidc`; +CREATE TABLE `oidc` ( + `issuer` text DEFAULT NULL, + `client_id` text DEFAULT NULL, + `secret_id` text DEFAULT NULL, + `enabled` tinyint(4) DEFAULT NULL, + `groupfilter` varchar(250) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- create repositories table +DROP TABLE IF EXISTS `repositories`; +CREATE TABLE `repositories` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(250) NOT NULL, + `user` varchar(250) DEFAULT NULL, + `password` text DEFAULT NULL, + `uri` varchar(250) DEFAULT NULL, + `description` text NOT NULL, + `use_for_forms` tinyint(4) DEFAULT NULL, + `use_for_playbooks` tinyint(4) DEFAULT NULL, + `cron` varchar(50) DEFAULT NULL, + `status` varchar(50) DEFAULT NULL, + `output` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `head` varchar(50) DEFAULT NULL, + `rebase_on_start` tinyint(4) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_AnsibleForms_repositories_natural_key` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- insert default values INSERT INTO AnsibleForms.azuread(client_id,secret_id,enable) VALUES('','',0); +INSERT INTO AnsibleForms.oidc(issuer, client_id,secret_id,enabled,groupfilter) VALUES('', '','',0,''); INSERT INTO AnsibleForms.groups(name) VALUES('admins'); -INSERT INTO AnsibleForms.awx(uri,token,username,password) VALUES('','','',''); +INSERT INTO AnsibleForms.awx(uri,token,username,password,ignore_certs,use_credentials,ca_bundle) VALUES('','','','',0,0,''); INSERT INTO AnsibleForms.users(username,password,email,group_id) VALUES('admin','$2b$10$Z/W0HXNBk2aLR4yVLkq5L..C8tXg.G.o1vkFr8D2lw8JSgWRCNiCa','',1); INSERT INTO AnsibleForms.ldap(server,port,ignore_certs,enable_tls,cert,ca_bundle,bind_user_dn,bind_user_pw,search_base,username_attribute,enable) VALUES('',389,1,0,'','','','','','sAMAccountName',0); -INSERT INTO AnsibleForms.settings(mail_server,mail_port,mail_secure,mail_username,mail_password,mail_from,url) VALUES('',25,0,'','','',''); +INSERT INTO AnsibleForms.settings(mail_server,mail_port,mail_secure,mail_username,mail_password,mail_from,url,forms_yaml) VALUES('',25,0,'','','','',''); + +-- enable foreign key checks SET FOREIGN_KEY_CHECKS=1; diff --git a/server/src/db/create_settings_table.sql b/server/src/db/create_settings_table.sql index 2830ea8d..c88d1e6b 100644 --- a/server/src/db/create_settings_table.sql +++ b/server/src/db/create_settings_table.sql @@ -7,6 +7,6 @@ CREATE TABLE `settings` ( `mail_password` text DEFAULT NULL, `mail_from` varchar(250) DEFAULT NULL, `url` varchar(250) DEFAULT NULL, - `forms_yaml` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `forms_yaml` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO AnsibleForms.settings(mail_server,mail_port,mail_secure,mail_username,mail_password,mail_from,url) VALUES('',25,0,'','','','',''); +INSERT INTO AnsibleForms.settings(mail_server,mail_port,mail_secure,mail_username,mail_password,mail_from,url,forms_yaml) VALUES('',25,0,'','','','',''); diff --git a/server/src/lib/mysql.js b/server/src/lib/mysql.js index f529220d..21ee8d75 100644 --- a/server/src/lib/mysql.js +++ b/server/src/lib/mysql.js @@ -23,6 +23,27 @@ MySql.query = async function (connection_name, query) { logger.debug(`[${connection_name}] query : ${query}`) var conn try{ + + // fixed in 5.0.4 + config.multipleStatements=true + // remove database if not defined + if(config.db_name){ + config.database = config.db_name + } + if(config.secure){ + config.ssl={ + sslmode:"required", + rejectUnauthorized:false + } + }else{ + config.ssl={ + sslmode:"none", + rejectUnauthorized:false + } + } + // get connection + config=MySql.clean(config) // remove unsupported properties + conn = await client.createConnection(MySql.clean(config)) var result try{ diff --git a/server/src/models/credential.model.js b/server/src/models/credential.model.js index 6069ef9c..e6579458 100644 --- a/server/src/models/credential.model.js +++ b/server/src/models/credential.model.js @@ -12,24 +12,29 @@ const cache = new NodeCache({ //credential object create var Credential=function(credential){ - this.name = credential.name; - this.host = credential.host; - this.port = credential.port; - this.user = credential.user; - this.db_name = credential.db_name; - this.secure = (credential.secure)?1:0; - this.is_database = (credential.is_database)?1:0; - this.password = encrypt(credential.password); - this.description = credential.description || ""; - this.db_type = credential.db_type; + if(credential.name!=undefined){this.name = credential.name } + if(credential.host!=undefined){this.host = credential.host } + if(credential.port!=undefined){this.port = credential.port } + if(credential.user!=undefined){this.user = credential.user } + if(credential.db_name!=undefined){this.db_name = credential.db_name } + if(credential.secure!=undefined){this.secure = (credential.secure)?1:0 } + if(credential.is_database!=undefined){this.is_database = (credential.is_database)?1:0 } + if(credential.password!=undefined){this.password = encrypt(credential.password) } + if(credential.description!=undefined){this.description = credential.description } + if(credential.db_type!=undefined){this.db_type = credential.db_type } }; Credential.create = async function (record) { + if(!record.name){ + throw "Name is required" + } logger.info(`Creating credential ${record.name}`) var res = await mysql.do("INSERT INTO AnsibleForms.`credentials` set ?", record) return res.insertId }; Credential.update = async function (record,id) { + const r = await Credential.findById(id) // quickly search name + record.name = r[0].name logger.info(`Updating credential ${record.name}`) var res = await mysql.do("UPDATE AnsibleForms.`credentials` set ? WHERE id=?", [record,id]) cache.del(record.name) diff --git a/server/src/models/form.model.js b/server/src/models/form.model.js index 76f6010c..2776339d 100644 --- a/server/src/models/form.model.js +++ b/server/src/models/form.model.js @@ -99,9 +99,9 @@ Form.load = async function() { var formfiles=[] var files=undefined - var ytt_env_data_opt = '' + var yttEnvDataOpt = '' if ('YTT_VARS_PREFIX' in process.env) { - ytt_env_data_opt = ` --data-values-env ${process.env.YTT_VARS_PREFIX}`; + yttEnvDataOpt = ` --data-values-env ${process.env.YTT_VARS_PREFIX}`; } var yttLibDataOpts = getYttLibDataOpts(); @@ -115,9 +115,9 @@ Form.load = async function() { try { if (appConfig.useYtt) { logger.info(`interpreting ${appFormsPath} with ytt.`); - logger.debug(`executing 'ytt -f ${appFormsPath} -f ${formslibdirpath}${ytt_env_data_opt}${yttLibDataOpts}'`) + logger.debug(`executing 'ytt -f ${appFormsPath} -f ${formslibdirpath}${yttEnvDataOpt}${yttLibDataOpts}'`) rawdata = execSync( - `ytt -f ${appFormsPath} -f ${formslibdirpath}${ytt_env_data_opt}${yttLibDataOpts}`, + `ytt -f ${appFormsPath} -f ${formslibdirpath}${yttEnvDataOpt}${yttLibDataOpts}`, { env: process.env, encoding: 'utf-8' @@ -127,7 +127,21 @@ Form.load = async function() { rawdata = fs.readFileSync(appFormsPath, 'utf8'); } } catch (e) { - logger.error(`failed to load '${appFormsPath}'.\n${e}`); + logger.error(`Failed to load '${appFormsPath}'.`,e); + + } + if(!rawdata){ + try{ + logger.warning("No forms found in database or forms.yaml... creating empty one from template") + var formsTemplatePath = path.join(__dirname,"../../templates/forms.yaml.template") + fs.copyFileSync(formsTemplatePath,appFormsPath) + logger.warning("File copied") + rawdata = fs.readFileSync(appFormsPath, 'utf8'); + } catch (e) { + logger.error(`Failed to copy forms from template and/or to load '${appFormsPath}'.`,e); + throw new Error(Helpers.getError(e,"Error reading the forms")) + } + } } // read extra form files @@ -143,9 +157,9 @@ Form.load = async function() { var itemRawData = ''; if (appConfig.useYtt) { logger.info(`interpreting ${itemFormPath} with ytt.`); - logger.debug(`executing 'ytt -f ${itemFormPath} -f ${formslibdirpath}${ytt_env_data_opt}'`) + logger.debug(`executing 'ytt -f ${itemFormPath} -f ${formslibdirpath}${yttEnvDataOpt}'`) itemRawData = execSync( - `ytt -f ${itemFormPath} -f ${formslibdirpath}${ytt_env_data_opt}`, + `ytt -f ${itemFormPath} -f ${formslibdirpath}${yttEnvDataOpt}`, { env: process.env, encoding: 'utf-8' @@ -159,7 +173,7 @@ Form.load = async function() { value: itemRawData }) }catch(e){ - logger.error(`failed to load file '${item}'.\n${e}`); + logger.error(`Failed to load file '${item}'`,e); } }); } @@ -186,7 +200,7 @@ Form.load = async function() { logger.error(`failed to parse file '${item.name}'.\n${e}`) } }) - if(!forms.forms){ + if(!forms?.forms){ forms.forms=[] } // merge extra files diff --git a/server/src/models/group.model.js b/server/src/models/group.model.js index 07f8d4d3..879c5f0f 100644 --- a/server/src/models/group.model.js +++ b/server/src/models/group.model.js @@ -15,13 +15,13 @@ Group.update = function (record,id) { logger.info(`Updating group ${record.name}`) return mysql.do("UPDATE AnsibleForms.`groups` set ? WHERE name=?", [record,id]) }; -Group.delete = function(id){ +Group.delete = async function(id){ if(id==1){ logger.warning("Someone is trying to remove the admins group !") - return new Promise.reject("You cannot delete group 'admins'") + throw new Error("You cannot delete group 'admins'") }else{ logger.info(`Deleting group ${id}`) - return mysql.do("DELETE FROM AnsibleForms.`groups` WHERE id = ? AND name<>'admins' AND NOT EXISTS(SELECT id FROM AnsibleForms.users u WHERE u.group_id=groups.id)", [id]) + return await mysql.do("DELETE FROM AnsibleForms.`groups` WHERE id = ? AND name<>'admins' AND NOT EXISTS(SELECT id FROM AnsibleForms.users u WHERE u.group_id=groups.id)", [id]) } }; diff --git a/server/src/models/repository.model.js b/server/src/models/repository.model.js index 91a9ca88..708cfc8b 100644 --- a/server/src/models/repository.model.js +++ b/server/src/models/repository.model.js @@ -44,6 +44,11 @@ Repository.update = function (record,name) { return res }) }; +Repository.reset = async function(name){ + logger.info(`Resetting repository ${name}`) + await Repo.delete(name) // delete the repo on disk + await Repository.clone(name) // recreate the repo +} Repository.delete = function(name){ logger.info(`Deleting repository ${name}`) Repo.delete(name) diff --git a/server/src/models/schema.model.js b/server/src/models/schema.model.js index a969955e..4e51f1d3 100644 --- a/server/src/models/schema.model.js +++ b/server/src/models/schema.model.js @@ -158,7 +158,6 @@ async function patchVersion4(messages,success,failed){ await checkPromise(addColumn("ldap","group_member_user_attribute","varchar(250)",true,"NULL"),messages,success,failed) // add column to have group member user attribute await checkPromise(addColumn("ldap","is_advanced","tinyint(4)",true,"0"),messages,success,failed) // is advanced config await checkPromise(addColumn("ldap","mail_attribute","varchar(250)",true,"NULL"),messages,success,failed) // add column to have mail attribute - // also the settings tables was not present before 4.0.0, it contains the URL and mail settings for notification // later the column forms_yaml was added to store the forms in yaml format (but that was in 5.x.x) buffer = fs.readFileSync(`${__dirname}/../db/create_settings_table.sql`) @@ -190,6 +189,9 @@ async function patchVersion5(messages,success,failed){ // on request, the awx_id was added to the jobs table, to later retrieve it for future tracking await checkPromise(addColumn("jobs","awx_id","int(11)",true,"NULL"),messages,success,failed) // add for future tracking + // patch for awx credentials, the use_credentials was added to the awx table, to allow the use of credentials + await checkPromise(addColumn("awx","use_credentials","tinyint(4)",true,"0"),messages,success,failed) // bugfix for awx credentials + // A new feature was added, the repositories table, to store the repositories for the forms and playbooks // A real gamechanger, because now the forms and playbooks can be stored in a git repository buffer = fs.readFileSync(`${__dirname}/../db/create_repositories_table.sql`) diff --git a/server/src/routes/repository.routes.js b/server/src/routes/repository.routes.js index e8cd3b30..bcd1e086 100644 --- a/server/src/routes/repository.routes.js +++ b/server/src/routes/repository.routes.js @@ -13,6 +13,8 @@ router.put('/:name', repositoryController.update); router.delete('/:name', repositoryController.delete); // Clone a repository by name router.post('/:name/clone/', repositoryController.clone); +// reset a repository by name +router.post('/:name/reset/', repositoryController.reset); // Pull a repository by name router.post('/:name/pull/', repositoryController.pull); diff --git a/server/src/swagger.json b/server/src/swagger.json index 0cc39896..458e3b39 100644 --- a/server/src/swagger.json +++ b/server/src/swagger.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "description": "This is the swagger interface for AnsibleForms.\r\nUse the `/auth/login` api with basic authentication to obtain a JWT token.\r\nThen use the access token, prefixed with the word '**Bearer**' to use all other api's.\r\nNote that the access token is limited in time. You can then either login again and get a new set of tokens or use the `/token` api and the refresh token to obtain a new set (preferred).", - "version": "5.0.3", + "version": "5.0.4", "title": "AnsibleForms", "contact": { "email": "info@ansibleforms.com" diff --git a/server/templates/cert.pem.template b/server/templates/cert.pem.template new file mode 100644 index 00000000..1d7f51c6 --- /dev/null +++ b/server/templates/cert.pem.template @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIJAMMW5u/nBgdrMA0GCSqGSIb3DQEBCwUAMHYxCzAJBgNV +BAYTAkJFMREwDwYDVQQIDAhCcnVzc2VsczERMA8GA1UEBwwIQnJ1c3NlbHMxEzAR +BgNVBAoMCkFuc2libGVHdXkxFTATBgNVBAsMDEFuc2libGVGb3JtczEVMBMGA1UE +AwwMYW5zaWJsZWZvcm1zMB4XDTIxMTEwMjE1MjIzMVoXDTMxMTAzMTE1MjIzMVow +djELMAkGA1UEBhMCQkUxETAPBgNVBAgMCEJydXNzZWxzMREwDwYDVQQHDAhCcnVz +c2VsczETMBEGA1UECgwKQW5zaWJsZUd1eTEVMBMGA1UECwwMQW5zaWJsZUZvcm1z +MRUwEwYDVQQDDAxhbnNpYmxlZm9ybXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCy4Gb5xWWG7w1CQ09m+PhG3kZaytv0nNs44q4rIBJNmphJ2tem8AIb +Ggg81SeuOW7e+Ze04IXzNGqEMJ+2I/Hq357a/SlSCL6HnW2c/hZ3CRdrHu1SyFk9 +YrbpWIOBPaJB0KEY5tn4SAds0WR7HUhDsd9/EgkV95mFm16EPfNIzGAdEAZgQkfi +GGUdfwPAUJoZlZzmSz2soxZJBFA0/x+cq21f0xrxesqM7Il4bWCZAVmYiIkAY1HA +YOy4C7DKDrPpvifPJdMiOHQ6fwP/JAOZ4HoyDYoUQDCBbAadK3ws4x6i8YlKfltm +Vq2t3zWZHSVwRF9abDbEGzmUSeYx91k3AgMBAAGjSzBJMA4GA1UdDwEB/wQEAwID +iDATBgNVHSUEDDAKBggrBgEFBQcDATAiBgNVHREEGzAZggxhbnNpYmxlZm9ybXOC +CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAHiY3mFawVyqeAN01jjGvF2+E +QjtHOL5Q4dtVdoZLvVFdXlRZkqtCZaQna3nYdzLlwWYQy8SC23QkU+P1wNAakf0N +na4M11Yi71h1hkHTo5Ub88DMWbz/VMaCo/Iefr4Sv1QoEmEeFEUtPbEAO6v9trqp +GOZv+6H3tuhuQkR+wWllBw7hqnWTvXTGRZXBlQH1wH04Vw6uUXg91ZMUE8DSddEz +nygVGVTEGWs3eld2j7rICRvGrtKOYrg6m+MfQN/skE1aa+auqu6OySAy0HBvS0u9 +G2+Ka6a54la0RR13lKmUR4y8B0izh5ThI0/FXtmVPI5XmG27Fellw7JG9bt1PQ== +-----END CERTIFICATE----- diff --git a/server/templates/forms.yaml.template b/server/templates/forms.yaml.template new file mode 100644 index 00000000..1ce7dc89 --- /dev/null +++ b/server/templates/forms.yaml.template @@ -0,0 +1,27 @@ +categories: # a list of categories to group forms + - name: Default + icon: bars +roles: # a list of roles + - name: admin + groups: + - local/admins + - name: public + groups: [] +constants: {} # free objects to re-use over all forms +forms: # a list of forms + - name: Demo Form + showHelp: true + help: > + This is a demo form + roles: + - public + description: A simple form + categories: + - Demo + icon: heart + playbook: dummy.yaml + type: ansible + fields: + - type: text + name: username + label: Username \ No newline at end of file diff --git a/server/templates/key.pem.template b/server/templates/key.pem.template new file mode 100644 index 00000000..93f34acd --- /dev/null +++ b/server/templates/key.pem.template @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy4Gb5xWWG7w1C +Q09m+PhG3kZaytv0nNs44q4rIBJNmphJ2tem8AIbGgg81SeuOW7e+Ze04IXzNGqE +MJ+2I/Hq357a/SlSCL6HnW2c/hZ3CRdrHu1SyFk9YrbpWIOBPaJB0KEY5tn4SAds +0WR7HUhDsd9/EgkV95mFm16EPfNIzGAdEAZgQkfiGGUdfwPAUJoZlZzmSz2soxZJ +BFA0/x+cq21f0xrxesqM7Il4bWCZAVmYiIkAY1HAYOy4C7DKDrPpvifPJdMiOHQ6 +fwP/JAOZ4HoyDYoUQDCBbAadK3ws4x6i8YlKfltmVq2t3zWZHSVwRF9abDbEGzmU +SeYx91k3AgMBAAECggEAYX0X4m0JBl9m9IRG1DJA7i7aXUVOV6TdfcVdczeJgi4N +bcMN4XfRTgAEGVN6yuOWX4PcgMIVfxVEMENn6BbzFDVIGMX9LS6C2NqeEQASMlIM +J1+1rHZw3JneYpLRKTD0K7aO9klq5nwrP81nXAn7hpl8235y4TwOudiRzLUO0M9Z +Da6DT5R7bgGfXpkwZKKzlZwLYZq/gw+TBtEXRN//k8cRDqGCNvetgYczinJrg3HQ ++PDayHAhMqm5ufr59cpGKGWScdNdnLAnporbSHd+6UHHm0Lpza3z2nI3z1R0B7xs +kgvKh8g4N3lcAkjzMYSYr6Sf+aL16GP7imM8I/O2kQKBgQDaNZVYxs0pbnKocNaU +YYqNkxT4eNxgU7h3Zqxq9dd++bVq4tOOpO7ocugBc1lrmbQ9kQKRtv3/nUgU9iCI +ohPjJZRBlvlIaQfPo6zDD3K4xtVe083nlyeKiEN7yslTRl3N9zQDN+Cmb0h6U0Id +g+q2hX8Ke1ofopQEKqZxpbxvswKBgQDR2vuNdfd77G/Qp7kOU6u+V7752W2nDLEu +f7FBlwB9tHkfGiS2G/8HZ90Ei9ZC9rDjlsib7HG8tV/EZMhAtKgta3MHJm3h8tEA +KtX3sRo9W+ArOp+jvu+6Bj7Jn+GOHXYwpOodP1zt47xPrnayFuPV7/5XtS2zrsuo +CwZyEuAObQKBgE2nwhWM8lhrSPyu43581A0cKdtfT7YsNTqw3G1YPi+e+DQosvdR +tQAeXHifr1P+qEk8wPhQckY0mAF1shBN9dvhdMh+zQo67p+zdPkaF06w3CBaKi3f ++h9v7OwyN8GeCiYRcn4utZEli1qVJLNSTgZUrehyC5m0hw6QixlozQ3HAoGBAItq +cuIo8/C1RBeXxb554chDnRF53Ho1WWSt2oHboqzgf/MkuCzv7n7qBpBlokO8hgm8 ++6ty6qDW0je0SMGMA4qhLrsaUbfhS+5ThvDWDLuk1QmDGdl8GOE6Eu56NCvo8MMi +XJJvrPox6MH7AsoPoO9ZUFzOdf1Aa/ZI1NBmL8oFAoGBALduRfv4J1kFvauCWueM +QKn7y8DIuFuPZiBmLIKPSPZlzayPLJjS99jRjX/f1l27wOVAbW2Jl6HcRgcZiyoo +BNT1HmUcnP/G9OljB76j/URVjcfAUY12R8bnfuGlb1gzXaUccg0NdXMhOELXgnvx +eO0iTAwAMg0zebMbkmfBe/Zw +-----END PRIVATE KEY-----