@@ -362,13 +365,13 @@
Can not be more than {{$v.form[field.name].$params.maxLength.max}} characters long
Value cannot be lower than {{$v.form[field.name].$params.minValue.min}}
Value cannot be higher than {{$v.form[field.name].$params.maxValue.max}}
- {{$v.form[field.name].$params.minSize.description}}
- {{$v.form[field.name].$params.maxSize.description}}
- {{$v.form[field.name].$params.regex.description}}
- {{$v.form[field.name].$params.validIf.description}}
- {{$v.form[field.name].$params.validIfNot.description}}
- {{$v.form[field.name].$params.notIn.description}}
- {{$v.form[field.name].$params.in.description}}
+ {{ replacePlaceholderInString($v.form[field.name].$params.minSize.description,true).value }}
+ {{ replacePlaceholderInString($v.form[field.name].$params.maxSize.description,true).value }}
+ {{ replacePlaceholderInString($v.form[field.name].$params.regex.description,true).value }}
+ {{ replacePlaceholderInString($v.form[field.name].$params.validIf.description,true).value }}
+ {{ replacePlaceholderInString($v.form[field.name].$params.validIfNot.description,true).value }}
+ {{ replacePlaceholderInString($v.form[field.name].$params.notIn.description,true).value }}
+ {{ replacePlaceholderInString($v.form[field.name].$params.in.description,true).value }}
Field must be identical to '{{$v.form[field.name].$params.sameAs.eq}}'
@@ -380,7 +383,7 @@
-
+
@@ -513,6 +516,7 @@
subjob:{}, // output of last subjob
dynamicFieldDependencies:{}, // which fields need to be re-evaluated if other fields change
dynamicFieldDependentOf:{}, // which fields are dependend from others
+ unevaluatedFields:[], // list of unevaluatedFields
defaults:{}, // holds default info per field
dynamicFieldStatus:{}, // holds the status of dynamics fields (running=currently being evaluated, variable=depends on others, fixed=only need 1-time lookup, default=has defaulted, undefined=trigger eval/query)
queryresults:{}, // holds the results of dynamic dropdown boxes
@@ -654,6 +658,14 @@
return obj
},
computed: {
+ // unevaluatedFieldsWarning
+ unevaluatedFieldsWarning(){
+ if(this.canSubmit){
+ return undefined
+ }else{
+ return this.unevaluatedFields.join(",") + " " + ((this.unevaluatedFields.length==1)?"is":"are") + " unevaluated..."
+ }
+ },
// joboutput
filteredJobOutput(){
if(!this.filterOutput) return this.jobResult?.data?.output?.replace(/\r\n/g,"
")
@@ -1396,6 +1408,7 @@
//console.log("enter loop");
// ref.$toast("... loop ...")
hasUnevaluatedFields=false; // reset flag
+ ref.unevaluatedFields=[]; // the list of unevaluatedfields - reset
// console.log("-------------------------------")
ref.currentForm.fields.forEach(
function(item,index){
@@ -1408,6 +1421,7 @@
// console.log("eval expression " + item.name)
// console.log(`[${item.name}][${flag}] : evaluating`)
hasUnevaluatedFields=true // set the un-eval flag if this is required
+ ref.unevaluatedFields.push(item.name)
// set flag running
ref.setFieldStatus(item.name,"running",false)
placeholderCheck = ref.replacePlaceholders(item) // check and replace placeholders
@@ -1524,6 +1538,7 @@
// console.log("eval query : " + item.name)
// set flag running
hasUnevaluatedFields=true
+ ref.unevaluatedFields.push(item.name)
ref.setFieldStatus(item.name,"running",false)
placeholderCheck = ref.replacePlaceholders(item) // check and replace placeholders
if(placeholderCheck.value!=undefined){ // expression is clean ?
@@ -1653,6 +1668,7 @@
if(ref.watchdog>50){ // is it taking too long ?
ref.jobResult.message="" // stop and reset
ref.$toast.warning("It took too long to evaluate all fields before run.\r\nLet the form stabilize and try again.")
+ ref.$toast.warning(ref.unevaluatedFieldsWarning)
}else{
//ref.$toast.info(`Stabilizing form...${ref.watchdog}`)
}
@@ -1839,58 +1855,120 @@
generateJsonOutput(filedata={}){
var ref=this
var formdata={}
- this.currentForm.fields.forEach((item, i) => {
- // this.checkDependencies(item) // hide field based on dependency
- if(this.visibility[item.name] && !item.noOutput){
- var fieldmodel = [].concat(item.model || [])
- var outputObject = item.outputObject || item.type=="expression" || item.type=="file" || item.type=="table" || false
- var outputValue = undefined
- // if uploaded file info, use that
- if(item.name in filedata){
- outputValue=filedata[item.name]
- // else just use the formdata
- }else{
- // deep clone, otherwise weird effects
- outputValue = Helpers.deepClone(this.form[item.name])
- }
- // if no model is given, we assign to the root
- if(!outputObject){ // do we need to flatten output ?
- outputValue=this.getFieldValue(outputValue,item.valueColumn || "",true)
- }
- if(fieldmodel.length==0){
- // deep clone = otherwise weird effects
- formdata[item.name]=Helpers.deepClone(outputValue)
- }else{
- fieldmodel.forEach((f)=>{
- // convert fieldmodel for actual object
- // svm.lif.name => svm["lif"].name = formvalue
- // using reduce, which is a recursive function
- f.split(/\s*\.\s*/).reduce((master,obj, level,arr) => {
- // if last
-
- if (level === (arr.length - 1)){
+ try{
+ this.currentForm.fields.forEach((item, i) => {
+ // this.checkDependencies(item) // hide field based on dependency
+ if(this.visibility[item.name] && !item.noOutput){
+ var fieldmodel = [].concat(item.model || [])
+ var outputObject = item.outputObject || item.type=="expression" || item.type=="file" || item.type=="table" || false
+ var outputValue = undefined
+ // if uploaded file info, use that
+ if(item.name in filedata){
+ outputValue=filedata[item.name]
+ // else just use the formdata
+ }else{
+ // deep clone, otherwise weird effects
+ outputValue = Helpers.deepClone(this.form[item.name])
+ }
+ // if no model is given, we assign to the root
+ if(!outputObject){ // do we need to flatten output ?
+ outputValue=this.getFieldValue(outputValue,item.valueColumn || "",true)
+ }
+ if(fieldmodel.length==0){
+ // deep clone = otherwise weird effects
+ formdata[item.name]=Helpers.deepClone(outputValue)
+ }else{
+
+ // here we build the full model object
+ fieldmodel.forEach((f)=>{
+ // convert fieldmodel for actual object
+ // svm.lif.name => svm["lif"].name = formvalue
+ // using reduce, which is a recursive function
+ f.split(/\s*\.\s*/).reduce((master,obj, level,arr) => {
+
+ var arrsplit = undefined
+
+ // if last
+ if (level === (arr.length - 1)){
+
+
// the last piece we assign the value to
- if(master[obj]===undefined){
- master[obj]=outputValue
- }else{
- master[obj]=Lodash.merge(master[obj],outputValue)
+ // if the last piece is an array, we need to create the array first
+ if(obj.match(/.*\[[0-9]+\]$/)){
+ // split the array name and index
+ arrsplit=obj.split(/\[([0-9]+)\]$/)
+ // if the array doesn't exist, we create it
+ if(master[arrsplit[0]]===undefined){
+ master[arrsplit[0]]=[]
+ }
+ // if the array index doesn't exist, we create it
+ if(master[arrsplit[0]][arrsplit[1]]===undefined){
+ master[arrsplit[0]][arrsplit[1]]={}
+ }
+ // assign the value to the array index
+ master[arrsplit[0]][arrsplit[1]]=outputValue
+
+ return master[arrsplit[0]][arrsplit[1]]
+
+ }else{ // just an object
+
+ // if the object doesn't exist, we create it
+ if(master[obj]===undefined){
+ master[obj]=outputValue
+ }else{
+ master[obj]=Lodash.merge(master[obj],outputValue)
+ }
+
+ // return the result for next reduce iteration
+ return master[obj]
+
}
-
- }else{
- // initialize first time to object
- if(master[obj]===undefined){
- master[obj]={}
+
+
+
+ }else{
+ // initialize first time to object or array
+
+ // if the object is an array, we need to create the array first
+ if(obj.match(/.*\[[0-9]+\]$/)){
+ // split the array name and index
+ arrsplit=obj.split(/\[([0-9]+)\]$/)
+ // if the array doesn't exist, we create it
+ if(master[arrsplit[0]]===undefined){
+ master[arrsplit[0]]=[]
+ }
+ // if the array index doesn't exist, we create it
+ if(master[arrsplit[0]][arrsplit[1]]===undefined){
+ master[arrsplit[0]][arrsplit[1]]={}
+ }
+
+ // return the result for next reduce iteration
+ return master[arrsplit[0]][arrsplit[1]]
+
+ }else{ // just an object
+
+ // if the object doesn't exist, we create it
+ if(master[obj]===undefined){
+ master[obj]={}
+ }
+ // return the result for next reduce iteration
+ return master[obj]
}
- }
- // return the result for next reduce iteration
- return master[obj]
- },formdata);
- })
+ }
+
+
+ },formdata);
+
+ })
+ }
}
- }
- });
+ });
+ }catch(err){
+ console.log(err)
+ ref.$toast.error("Failed to generate json output.\r\nContact the developer.\r\n" + (err.message || err.toString()))
+ }
// update main data
Vue.set(this,"formdata",formdata)
},
diff --git a/client/src/router.js b/client/src/router.js
index d9cdf4be..3dba4521 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -2,6 +2,7 @@ import Vue from 'vue'
import Router from 'vue-router'
import Forms from './views/Forms.vue'
import Form from './views/Form.vue'
+import FormReload from './views/FormReload.vue'
import Login from './views/Login.vue'
import ErrorVue from './views/Error.vue'
import Schema from './views/Schema.vue'
@@ -60,6 +61,10 @@ export default new Router({
name:"Form",
component:Form
},
+ {
+ path: '/form-reload',
+ component:FormReload
+ },
{
path:"/login",
name:"Login",
diff --git a/client/src/views/FormReload.vue b/client/src/views/FormReload.vue
new file mode 100644
index 00000000..6181d9ed
--- /dev/null
+++ b/client/src/views/FormReload.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/client/src/views/Ldap.vue b/client/src/views/Ldap.vue
index 2397d59a..4e3a1db4 100644
--- a/client/src/views/Ldap.vue
+++ b/client/src/views/Ldap.vue
@@ -25,6 +25,7 @@
+
@@ -79,6 +80,7 @@
groups_search_base: "",
group_class: "",
group_member_attribute: "",
+ mail_attribute: "",
group_member_user_attribute: "",
cert:"",
ca_bundle:"",
@@ -150,6 +152,9 @@
groups_attribute:{
required
},
+ mail_attribute:{
+ required
+ },
cert:{
requiredIf:requiredIf(function(ldap){
return ldap.enable_tls && !ldap.ignore_certs
diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue
index 74c883b5..35d0b33d 100644
--- a/client/src/views/Login.vue
+++ b/client/src/views/Login.vue
@@ -15,7 +15,7 @@
@@ -45,6 +45,7 @@
username: "",
password: ""
},
+ baseUrl:"/",
azureAdEnabled:false,
azureGroupfilter:"",
azureGraphUrl:"https://graph.microsoft.com"
@@ -53,7 +54,7 @@
methods: {
getSettings(azuretoken){
var ref=this
- axios.get(`${process.env.BASE_URL}api/v1/auth/settings`)
+ axios.get(`${ref.baseUrl}api/v1/auth/settings`)
.then((result)=>{
if(result.data?.status=='success'){
this.azureAdEnabled=!!result.data.data.output.azureAdEnabled
@@ -100,7 +101,7 @@
console.log("Groups have been filtered")
}
// No more nextLink, you have all the groups
- axios.post(`${process.env.BASE_URL}api/v1/auth/azureadoauth2/login`, { azuretoken, groups:allGroups })
+ axios.post(`${ref.baseUrl}api/v1/auth/azureadoauth2/login`, { azuretoken, groups:allGroups })
.then((result) => {
if (result.data.token) {
TokenStorage.storeToken(result.data.token);
@@ -121,14 +122,14 @@
});
},
login() {
-
+ var ref=this
if (!this.$v.user.$invalid) {
console.log("Logging in")
- var basicAuth = 'Basic ' + btoa(this.user.username + ':' + this.user.password);
+ var basicAuth = 'Basic ' + new Buffer(this.user.username + ':' + this.user.password).toString('base64')
var postconfig={
headers:{'Authorization':basicAuth}
}
- axios.post(`${process.env.BASE_URL}api/v1/auth/login`,{},postconfig)
+ axios.post(`${ref.baseUrl}api/v1/auth/login`,{},postconfig)
.then((result)=>{
if(result.data.token){
console.log("Login success, storing tokens")
@@ -158,7 +159,7 @@
},
mounted(){
-
+ this.baseUrl = process.env.BASE_URL
if(this.$route.query.azuretoken){
this.loading=true
this.getSettings(this.$route.query.azuretoken)
diff --git a/client/src/views/Users.vue b/client/src/views/Users.vue
index 3f2db4c0..832f434d 100644
--- a/client/src/views/Users.vue
+++ b/client/src/views/Users.vue
@@ -38,6 +38,7 @@
+
@@ -91,6 +92,7 @@
username:"",
password:"",
password2:"",
+ email:"",
group_id:undefined
},
showDelete:false,
@@ -154,6 +156,7 @@
username:"",
password:"",
password2:"",
+ email:"",
group_id:undefined
}
}
@@ -241,6 +244,9 @@
(value) => !helpers.req(value) || (new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\\$%\\^&\\*])").test(value))
)
},
+ email:{
+ email
+ },
group_id: {
required
},
diff --git a/client/vue.config.js b/client/vue.config.js
index dbfd4d46..690c9ccd 100644
--- a/client/vue.config.js
+++ b/client/vue.config.js
@@ -3,6 +3,7 @@
/**
* @type {import('@vue/cli-service').ProjectOptions}
*/
+ require("dotenv").config()
const configureAPI = require('./../server/src/configure')
const appConfig = require('./../server/config/app.config')
diff --git a/docs/_data/help.yaml b/docs/_data/help.yaml
index a2eddcfa..5652cc60 100644
--- a/docs/_data/help.yaml
+++ b/docs/_data/help.yaml
@@ -2324,7 +2324,7 @@
Enforces a validation where the current field can not be one of the values in a list (referencing another field).
This field requires an object with 2 attributes:
* `field` : The name of the field that holds the list
- * `description` : A validation message to show when the validation is not met.
+ * `description` : A validation message to show when the validation is not met. (**new in v5.0.1** : can have placeholder)
with_types: text, expression, password, datetime
examples:
- name: Check if ip exists
@@ -2341,7 +2341,7 @@
Enforces a validation where the current field must be one of the values in a list (referencing another field).
This field requires an object with 2 attributes:
* `field` : The name of the field that holds the list
- * `description` : A validation message to show when the validation is not met.
+ * `description` : A validation message to show when the validation is not met. (**new in v5.0.1** : can have placeholder)
with_types: text, expression, password, datetime
examples:
- name: Check if ip is in range
@@ -2358,7 +2358,7 @@
Enforces a validation where a referencing (expression) field must be true.
This field requires an object with 2 attributes:
* `field` : The name of the referencing field
- * `description` : A validation message to show when the validation is not met.
+ * `description` : A validation message to show when the validation is not met. (**new in v5.0.1** : can have placeholder)
examples:
- name: Ip must be pingable
code: |
@@ -2374,7 +2374,7 @@
Enforces a validation where a referencing (expression) field must be false.
This field requires an object with 2 attributes:
* `field` : The name of the referencing field
- * `description` : A validation message to show when the validation is not met.
+ * `description` : A validation message to show when the validation is not met. (**new in v5.0.1** : can have placeholder)
examples:
- name: Check if powered off
code: |
@@ -2432,19 +2432,21 @@
- version: 4.0.9
type: added
description: model can now be an array
+ - version: 5.0.1
+ type: added
+ description: you can now model arrays
examples:
- name: Model 2 fields together as a single object
code: |
- name: diskName
type: text
- model: disk.name
+ model: disk[0].name
- name: diskSize
type: number
- model: disk.size
-
+ model: disk[0].size
# the extravars of this example will be like this :
disk:
- name: mydisk
+ - name: mydisk
size: 123
- name: asCredential
type: boolean
@@ -3089,7 +3091,7 @@
Enforces a validation where the current field can not be one of the values in a list (referencing another field).
This field requires an object with 2 attributes:
* `field` : The name of the field that holds the list
- * `description` : A validation message to show when the validation is not met.
+ * `description` : A validation message to show when the validation is not met. (**new in v5.0.1** : can have placeholder)
with_types: text, expression, password, datetime
examples:
- name: Check if ip exists
@@ -3135,7 +3137,7 @@
Enforces a validation where the current field must be one of the values in a list (referencing another field).
This field requires an object with 2 attributes:
* `field` : The name of the field that holds the list
- * `description` : A validation message to show when the validation is not met.
+ * `description` : A validation message to show when the validation is not met. (**new in v5.0.1** : can have placeholder)
with_types: text, expression, password, datetime
examples:
- name: Check if ip is in range
diff --git a/server/Dockerfile b/server/Dockerfile
index 5eca66e9..a5918d36 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -20,7 +20,7 @@ RUN apk add py3-pip
# install some python dependencies
RUN pip3 install requests six
-RUN apk add --update --no-cache --virtual .build-deps g++ gcc libxml2-dev libxslt-dev python3-dev
+RUN apk add --update --no-cache --virtual .build-deps g++ gcc libxml2-dev libxslt-dev unixodbc-dev python3-dev postgresql-dev
RUN apk add --no-cache libxslt
RUN apk add --no-cache mysql-client
RUN apk add --no-cache curl
diff --git a/server/package.json b/server/package.json
index 5a6be612..a1ea0ae0 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "ansible_forms",
- "version": "5.0.0",
+ "version": "5.0.1",
"repository": {
"type": "git",
"url": "git://github.com/ansibleguy76/ansibleforms.git"
@@ -17,36 +17,36 @@
"clean": "rimraf dist"
},
"dependencies": {
- "core-js": "~3.34.0",
- "axios": "~1.6.2",
+ "core-js": "~3.36.1",
+ "axios": "~1.6.8",
"cors": "~2.8.5",
"bcrypt": "~5.1.0",
"cheerio": "~1.0.0-rc.12",
"connect-history-api-fallback": "~2.0.0",
- "express": "~4.18.2",
+ "express": "~4.19.2",
"multer": "~1.4.5-lts.1",
- "express-session": "~1.17.3",
- "winston": "~3.11.0",
+ "express-session": "~1.18.0",
+ "winston": "~3.13.0",
"winston-syslog": "~2.7.0",
- "winston-daily-rotate-file": "~4.7.1",
+ "winston-daily-rotate-file": "~5.0.0",
"passport": "~0.7.0",
"modern-passport-http": "~0.3.0",
"passport-jwt": "~4.0.1",
"@outlinewiki/passport-azure-ad-oauth2": "~0.1.0",
- "mysql2": "~3.6.5",
+ "mysql2": "~3.9.4",
"ajv": "~6.12.6",
"ajv-error-parser": "~1.0.7",
"lodash": "~4.17.21",
"bluebird": "~3.7.2",
- "node-jq": "~4.2.2",
- "moment": "~2.29.4",
+ "node-jq": "~4.3.0",
+ "moment": "~2.30.1",
"yaml": "~2.3.4",
"cert-info":"~1.5.1",
"thenby":"~1.3.4",
- "mssql":"~10.0.1",
- "mongodb":"~6.3.0",
+ "mssql":"~10.0.2",
+ "mongodb":"~6.5.0",
"pg":"~8.11.3",
- "ip":"1.1.8",
+ "ip":"2.0.1",
"dayjs": "1.11.10",
"fs-extra":"~11.2.0",
"node-cache":"~5.1.2",
@@ -54,18 +54,18 @@
"ldapjs": "~3.0.7",
"read-last-lines": "~1.8.0",
"swagger-ui-express": "~5.0.0",
- "nodemailer": "~6.9.7",
+ "nodemailer": "~6.9.8",
"cron-parser": "~4.9.0"
},
"devDependencies": {
- "@babel/core": "7.23.6",
+ "@babel/core": "7.23.7",
"@babel/cli": "~7.23.4",
"@babel/node": "~7.22.19",
"@babel/eslint-parser": "7.23.3",
- "nodemon": "~3.0.2",
+ "nodemon": "~3.0.3",
"rifraf": "~2.0.3",
"npm-run-all": "*",
- "dotenv": "~16.3.1",
+ "dotenv": "~16.4.1",
"eslint": "~8.56.0"
},
"eslintConfig": {
diff --git a/server/src/auth/auth_basic.js b/server/src/auth/auth_basic.js
index dd6bbcae..5e1d9f82 100644
--- a/server/src/auth/auth_basic.js
+++ b/server/src/auth/auth_basic.js
@@ -20,6 +20,9 @@ passport.use(
// user found in db
if(!result.isValid) throw "Wrong password"
user.username = result.user.username
+ if(result.user.email){
+ user.email = result.user.email
+ }
user.id = result.user.id
user.type = 'local'
user.groups = User.getGroups(user,result.user.groups)
@@ -49,6 +52,7 @@ passport.use(
// logger.debug(JSON.stringify(result))
var user = {}
user.username = result[ldapConfig.username_attribute]
+ user.email = result[ldapConfig.mail_attribute]
user.type = 'ldap'
user.groups = User.getGroups(user,result,ldapConfig)
user.roles = await User.getRoles(user.groups,user)
diff --git a/server/src/controllers/expression.controller.js b/server/src/controllers/expression.controller.js
index 637840c3..8e0d94db 100644
--- a/server/src/controllers/expression.controller.js
+++ b/server/src/controllers/expression.controller.js
@@ -21,7 +21,7 @@ exports.execute = function(req, res) {
//
}
}
- res.json(new RestResult("success","successfully executed expression " + expression,result))
+ res.json(new RestResult("success","successfully executed expression " + expression,result,undefined,true))
})
.catch((err)=>{
logger.error(`Error in expression : ${err}`)
diff --git a/server/src/controllers/login.controller.js b/server/src/controllers/login.controller.js
index 6550bfad..545156f2 100644
--- a/server/src/controllers/login.controller.js
+++ b/server/src/controllers/login.controller.js
@@ -83,7 +83,7 @@ exports.basic = async function(req, res,next) {
}
// send the tokens to the requester
// if admin role, you can override the expirydays (for accesstoken only)
- if(req.query.expiryDays && user?.roles?.includes("admin") && !isNan(req.query.expiryDays)){
+ if(req.query.expiryDays && user?.roles?.includes("admin") && !isNaN(req.query.expiryDays)){
return res.json(userToJwt(user,`${req.query.expiryDays}D`))
}
return res.json(userToJwt(user));
diff --git a/server/src/db/create_schema_and_tables.sql b/server/src/db/create_schema_and_tables.sql
index b086117b..cdece659 100644
--- a/server/src/db/create_schema_and_tables.sql
+++ b/server/src/db/create_schema_and_tables.sql
@@ -13,6 +13,7 @@ CREATE TABLE `users`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
+ `email` varchar(255) NOT NULL,
`group_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_AnsibleForms_users_natural_key` (`username`),
@@ -54,6 +55,13 @@ CREATE TABLE `ldap` (
`bind_user_pw` text DEFAULT NULL,
`search_base` varchar(250) DEFAULT NULL,
`username_attribute` varchar(250) DEFAULT NULL,
+ `groups_search_base` varchar(250) DEFAULT NULL,
+ `groups_attribute` varchar(250) DEFAULT NULL,
+ `group_class` varchar(250) DEFAULT NULL,
+ `group_member_attribute` varchar(250) DEFAULT NULL,
+ `group_member_user_attribute` varchar(250) DEFAULT NULL,
+ `is_advanced` tinyint(4) DEFAULT NULL,
+ `mail_attribute` varchar(250) DEFAULT NULL,
`enable` tinyint(4) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `awx`;
@@ -117,7 +125,7 @@ CREATE TABLE `azuread` (
INSERT INTO AnsibleForms.azuread(client_id,secret_id,enable) VALUES('','',0);
INSERT INTO AnsibleForms.groups(name) VALUES('admins');
INSERT INTO AnsibleForms.awx(uri,token,username,password) VALUES('','','','');
-INSERT INTO AnsibleForms.users(username,password,group_id) VALUES('admin','$2b$10$Z/W0HXNBk2aLR4yVLkq5L..C8tXg.G.o1vkFr8D2lw8JSgWRCNiCa',1);
+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,'','','','');
SET FOREIGN_KEY_CHECKS=1;
diff --git a/server/src/lib/common.js b/server/src/lib/common.js
index 8153f51a..93b82e1a 100644
--- a/server/src/lib/common.js
+++ b/server/src/lib/common.js
@@ -2,6 +2,7 @@ const Certinfo=require("cert-info")
const restResult=require("../models/restResult.model")
const logger=require("./logger")
const config=require("../../config/app.config")
+const Help = require("../models/help.model")
var Helpers = function(){
}
diff --git a/server/src/lib/ldap-authentication.js b/server/src/lib/ldap-authentication.js
index d2c26dd9..0775fff3 100644
--- a/server/src/lib/ldap-authentication.js
+++ b/server/src/lib/ldap-authentication.js
@@ -4,6 +4,20 @@ const ldap = require('ldapjs')
// convert a SearchResultEntry object in ldapjs 3.0
// to a user object to maintain backward compatibility
+// added by Mirko for 5.0.1 => ΓΌ in results
+function unescapeLdapResult(ldapResult) {
+ // Regular expression to match the escaped sequences
+ const regex = /\\([0-9a-fA-F]{2})\\([0-9a-fA-F]{2})/g;
+
+ // Replace each escaped sequence with its Unicode character
+ return ldapResult.replace(regex, (match, p1, p2) => {
+ // Convert the hex codes to a Buffer
+ const bytes = Buffer.from([parseInt(p1, 16), parseInt(p2, 16)]);
+ // Convert the Buffer to a UTF-8 String
+ return bytes.toString('utf8');
+ });
+}
+
function _searchResultToUser(pojo) {
assert(pojo.type == 'SearchResultEntry')
let user = { dn: pojo.objectName }
@@ -85,7 +99,7 @@ async function _searchUser(
let searchOptions = {
filter: filter,
scope: 'sub',
- attributes: attributes,
+ attributes: attributes
}
if (attributes) {
searchOptions.attributes = attributes
@@ -212,6 +226,7 @@ async function authenticateWithAdmin(
)
}
var userDn = user.dn
+ userDn = unescapeLdapResult(userDn)
let ldapUserClient
try {
ldapUserClient = await _ldapBind(userDn, userPassword, starttls, ldapOpts)
diff --git a/server/src/lib/logger.js b/server/src/lib/logger.js
index c8691d8c..d05443bc 100644
--- a/server/src/lib/logger.js
+++ b/server/src/lib/logger.js
@@ -23,30 +23,45 @@ const formatNoColor = winston.format.combine(
),
)
+const transportConsole = new winston.transports.Console({
+ stderrLevels: ["error"],
+ level:loggerConfig.consolelevel,
+ format:formatColor
+});
+
+const transportDailyRotateFileErrors = new winston.transports.DailyRotateFile({
+ filename: loggerConfig.path + "/ansibleforms.errors.%DATE%.log",
+ datePattern: 'YYYY-MM-DD',
+ maxFiles: '30d',
+ zippedArchive: true,
+ level: 'error',
+ format:formatNoColor
+});
+
+const transportDailyRotateFile = new winston.transports.DailyRotateFile({
+ level: loggerConfig.level,
+ filename: loggerConfig.path + "/ansibleforms.%DATE%.log",
+ datePattern: 'YYYY-MM-DD',
+ zippedArchive: true,
+ maxFiles: '30d',
+ format:formatColor
+});
+
var transports = [
- new winston.transports.Console({
- stderrLevels: ["error"],
- level:loggerConfig.consolelevel,
- format:formatColor
- }),
- new winston.transports.DailyRotateFile({
- filename: loggerConfig.path + "/ansibleforms.errors.%DATE%.log",
- datePattern: 'YYYY-MM-DD',
- maxFiles: '30d',
- zippedArchive: true,
- level: 'error',
- format:formatNoColor
- }),
- new winston.transports.DailyRotateFile({
- level: loggerConfig.level,
- filename: loggerConfig.path + "/ansibleforms.%DATE%.log",
- datePattern: 'YYYY-MM-DD',
- zippedArchive: true,
- maxFiles: '30d',
- format:formatColor
- }),
+ transportConsole,
+ transportDailyRotateFileErrors,
+ transportDailyRotateFile,
]
+transportDailyRotateFile.on('error', error => {
+ console.error('Error in transportDailyRotateFile:', error);
+});
+
+transportDailyRotateFileErrors.on('error', error => {
+ console.error('Error in transportDailyRotateFileErrors:', error);
+});
+
+
if(loggerConfig.sysloghost){
transports.push(
new winston.transports.Syslog({
diff --git a/server/src/models/job.model.js b/server/src/models/job.model.js
index f9f764c7..2e309d30 100644
--- a/server/src/models/job.model.js
+++ b/server/src/models/job.model.js
@@ -871,16 +871,7 @@ Ansible.launch=async (ev,credentials,jobid,counter,approval,approved=false)=>{
}else{
counter++
}
- if(approval){
- if(!approved){
- await Job.sendApprovalNotification(approval,ev,jobid)
- await Job.printJobOutput(`APPROVE [${playbook}] ${'*'.repeat(69-playbook.length)}`,"stdout",jobid,++counter)
- await Job.update({status:"approve",approval:JSON.stringify(approval),end:moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')},jobid)
- return true
- }else{
- logger.notice("Continuing ansible " + playbook + " it has been approved")
- }
- }
+
// we make a copy, we don't want to mutate the original
var extravars={...ev}
// ansible can have multiple inventories
@@ -907,7 +898,16 @@ Ansible.launch=async (ev,credentials,jobid,counter,approval,approved=false)=>{
var keepExtravars = extravars?.__keepExtravars__ || false
var diff = extravars?.__diff__ || false
var ansibleCredentials = extravars?.__ansibleCredentials__ || ""
-
+ if(approval){
+ if(!approved){
+ await Job.sendApprovalNotification(approval,ev,jobid)
+ await Job.printJobOutput(`APPROVE [${playbook}] ${'*'.repeat(69-playbook.length)}`,"stdout",jobid,++counter)
+ await Job.update({status:"approve",approval:JSON.stringify(approval),end:moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')},jobid)
+ return true
+ }else{
+ logger.notice("Continuing ansible " + playbook + " it has been approved")
+ }
+ }
// merge credentials now
extravars = {...extravars,...credentials}
// convert to string for the command
@@ -997,16 +997,7 @@ Awx.launch = async function (ev,credentials,jobid,counter,approval,approved=fals
}else{
counter++
}
- if(approval){
- if(!approved){
- await Job.sendApprovalNotification(approval,ev,jobid)
- await Job.printJobOutput(`APPROVE [${template}] ${'*'.repeat(69-template.length)}`,"stdout",jobid,++counter)
- await Job.update({status:"approve",approval:JSON.stringify(approval),end:moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')},jobid)
- return true
- }else{
- logger.notice("Continuing awx " + template + " it has been approved")
- }
- }
+
// we make a copy, we don't mutate the original
var extravars={...ev}
@@ -1021,7 +1012,16 @@ Awx.launch = async function (ev,credentials,jobid,counter,approval,approved=fals
var diff = extravars?.__diff__ || false
var template = extravars?.__template__
var awxCredentials = extravars?.__awxCredentials__ || []
-
+ if(approval){
+ if(!approved){
+ await Job.sendApprovalNotification(approval,ev,jobid)
+ await Job.printJobOutput(`APPROVE [${template}] ${'*'.repeat(69-template.length)}`,"stdout",jobid,++counter)
+ await Job.update({status:"approve",approval:JSON.stringify(approval),end:moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')},jobid)
+ return true
+ }else{
+ logger.notice("Continuing awx " + template + " it has been approved")
+ }
+ }
try{
const jobTemplate = await Awx.findJobTemplateByName(template)
logger.debug("Found jobtemplate, id = " + jobTemplate.id)
diff --git a/server/src/models/ldap.model.js b/server/src/models/ldap.model.js
index 42a9da68..14749b40 100644
--- a/server/src/models/ldap.model.js
+++ b/server/src/models/ldap.model.js
@@ -24,6 +24,7 @@ var Ldap=function(ldap){
this.group_class = (ldap.is_advanced)?ldap.group_class:""
this.group_member_attribute = (ldap.is_advanced)?ldap.group_member_attribute:""
this.group_member_user_attribute = (ldap.is_advanced)?ldap.group_member_user_attribute:""
+ this.mail_attribute = ldap.mail_attribute
};
Ldap.update = function (record) {
logger.info(`Updating ldap ${record.server}`)
diff --git a/server/src/models/restResult.model.js b/server/src/models/restResult.model.js
index 4fa39a5d..06fcfcfa 100644
--- a/server/src/models/restResult.model.js
+++ b/server/src/models/restResult.model.js
@@ -1,8 +1,8 @@
'use strict';
//awx object create - not used, but you could create an instance with it
-var RestResult=function(status,message,output,error){
- if(!output){
+var RestResult=function(status,message,output,error,forExpression=false){
+ if(!output && !forExpression){
if(output!==undefined) output=""
}
if(!error){
diff --git a/server/src/models/schema.model.js b/server/src/models/schema.model.js
index 5f4e827d..c3fecd66 100644
--- a/server/src/models/schema.model.js
+++ b/server/src/models/schema.model.js
@@ -266,12 +266,15 @@ function patchAll(){
tablePromises.push(setUtf8mb4CharacterSet("jobs","approval","longtext")) // allow emoticon or utf16 character
tablePromises.push(setUtf8mb4CharacterSet("job_output","output","longtext")) // allow emoticon or utf16 character
// patches to db for v4.0.20
- tablePromises.push(addColumn("ldap","groups_search_base","varchar(255)",true,"NULL")) // add column to have groups search base
- tablePromises.push(addColumn("ldap","groups_attribute","varchar(255)",false,"'memberOf'")) // add column to have groups attribute
- tablePromises.push(addColumn("ldap","group_class","varchar(255)",true,"NULL")) // add column to have group class
- tablePromises.push(addColumn("ldap","group_member_attribute","varchar(255)",true,"NULL")) // add column to have group member attribute
- tablePromises.push(addColumn("ldap","group_member_user_attribute","varchar(255)",true,"NULL")) // add column to have group member user attribute
+ tablePromises.push(addColumn("ldap","groups_search_base","varchar(250)",true,"NULL")) // add column to have groups search base
+ tablePromises.push(addColumn("ldap","groups_attribute","varchar(250)",false,"'memberOf'")) // add column to have groups attribute
+ tablePromises.push(addColumn("ldap","group_class","varchar(250)",true,"NULL")) // add column to have group class
+ tablePromises.push(addColumn("ldap","group_member_attribute","varchar(250)",true,"NULL")) // add column to have group member attribute
+ tablePromises.push(addColumn("ldap","group_member_user_attribute","varchar(250)",true,"NULL")) // add column to have group member user attribute
tablePromises.push(addColumn("ldap","is_advanced","tinyint(4)",true,"0")) // is advanced config
+ // patches to db for v5.0.1
+ tablePromises.push(addColumn("ldap","mail_attribute","varchar(250)",true,"NULL")) // add column to have mail attribute
+ tablePromises.push(addColumn("users","email","varchar(250)",true,"NULL")) // add column to have email
buffer = fs.readFileSync(`${__dirname}/../db/create_settings_table.sql`)
sql = buffer.toString()
tablePromises.push(addTable("settings",sql)) // add settings table
diff --git a/server/src/models/user.model.js b/server/src/models/user.model.js
index 949e6373..02a739ec 100644
--- a/server/src/models/user.model.js
+++ b/server/src/models/user.model.js
@@ -14,6 +14,9 @@ var User=function(user){
if(user.username!=undefined){
this.username = user.username;
}
+ if(user.email!=undefined){
+ this.email = user.email;
+ }
if(user.password!=undefined){
this.password = user.password;
}
@@ -72,7 +75,7 @@ User.findByUsername = function (username) {
};
User.authenticate = function (username,password) {
logger.info(`Checking password for user ${username}`)
- var query = "SELECT users.id,`username`,`password`,GROUP_CONCAT(groups.name) `groups` FROM AnsibleForms.`users`,AnsibleForms.`groups` WHERE `users`.group_id=`groups`.id AND username=?;"
+ var query = "SELECT users.*,GROUP_CONCAT(groups.name) `groups` FROM AnsibleForms.`users`,AnsibleForms.`groups` WHERE `users`.group_id=`groups`.id AND username=?;"
return mysql.do(query,username)
.then((res)=>{
if(res.length > 0 && res[0].password){
@@ -248,7 +251,7 @@ User.checkLdap = function(username,password){
throw "Certificate is not valid"
}else{
logger.info(`Checking ldap for user ${username}`)
- // logger.debug(JSON.stringify(options))
+ logger.debug(JSON.stringify(options))
return ldapAuthentication(options)
}
})
diff --git a/server/src/swagger.json b/server/src/swagger.json
index c2ade4a7..44e723ea 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.0",
+ "version": "5.0.1",
"title": "AnsibleForms",
"contact": {
"email": "info@ansibleforms.com"