@saltcorn/server 0.9.6-beta.9 → 0.9.7-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/app.js +9 -2
  2. package/auth/admin.js +51 -52
  3. package/auth/roleadmin.js +6 -2
  4. package/auth/routes.js +28 -10
  5. package/auth/testhelp.js +86 -0
  6. package/help/Field label.tmd +11 -0
  7. package/help/Field types.tmd +39 -0
  8. package/help/Inclusion Formula.tmd +38 -0
  9. package/help/Ownership field.tmd +76 -0
  10. package/help/Ownership formula.tmd +75 -0
  11. package/help/Table roles.tmd +20 -0
  12. package/help/User groups.tmd +35 -0
  13. package/help/User roles.tmd +30 -0
  14. package/load_plugins.js +28 -4
  15. package/locales/en.json +28 -1
  16. package/locales/it.json +3 -2
  17. package/markup/forms.js +5 -1
  18. package/package.json +9 -9
  19. package/public/log_viewer_utils.js +32 -0
  20. package/public/mermaid.min.js +705 -306
  21. package/public/saltcorn-builder.css +23 -0
  22. package/public/saltcorn-common.js +195 -71
  23. package/public/saltcorn.css +72 -0
  24. package/public/saltcorn.js +78 -0
  25. package/restart_watcher.js +1 -0
  26. package/routes/actions.js +27 -0
  27. package/routes/admin.js +180 -66
  28. package/routes/api.js +6 -0
  29. package/routes/common_lists.js +42 -32
  30. package/routes/fields.js +9 -1
  31. package/routes/homepage.js +2 -0
  32. package/routes/menu.js +69 -4
  33. package/routes/notifications.js +90 -10
  34. package/routes/pageedit.js +18 -13
  35. package/routes/plugins.js +5 -1
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +47 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +20 -6
  40. package/routes/viewedit.js +11 -7
  41. package/serve.js +27 -5
  42. package/tests/edit.test.js +426 -0
  43. package/tests/fields.test.js +21 -0
  44. package/tests/filter.test.js +68 -0
  45. package/tests/page.test.js +2 -2
  46. package/tests/sync.test.js +59 -0
  47. package/wrapper.js +4 -1
package/app.js CHANGED
@@ -50,6 +50,7 @@ const locales = Object.keys(available_languages);
50
50
  const i18n = new I18n({
51
51
  locales,
52
52
  directory: path.join(__dirname, "locales"),
53
+ mustacheConfig: { disable: true },
53
54
  });
54
55
  // jwt config
55
56
  const jwt_secret = db.connectObj.jwt_secret;
@@ -171,9 +172,13 @@ const getApp = async (opts = {}) => {
171
172
  const tenants = await getAllTenants();
172
173
  await init_multi_tenant(loadAllPlugins, opts.disableMigrate, tenants);
173
174
  }
175
+ const pruneSessionInterval = +getState().getConfig(
176
+ "prune_session_interval",
177
+ 900
178
+ );
174
179
  //
175
180
  // todo ability to configure session_secret Age
176
- app.use(getSessionStore());
181
+ app.use(getSessionStore(pruneSessionInterval));
177
182
 
178
183
  app.use(passport.initialize());
179
184
  app.use(passport.authenticate(["jwt", "session"]));
@@ -297,7 +302,9 @@ const getApp = async (opts = {}) => {
297
302
  if (
298
303
  u &&
299
304
  u.last_mobile_login &&
300
- u.last_mobile_login <= jwt_payload.iat
305
+ (typeof u.last_mobile_login === "string"
306
+ ? new Date(u.last_mobile_login).valueOf()
307
+ : u.last_mobile_login) <= jwt_payload.iat
301
308
  ) {
302
309
  return done(null, {
303
310
  email: u.email,
package/auth/admin.js CHANGED
@@ -99,6 +99,10 @@ const getUserFields = async (req) => {
99
99
  input_type: "email",
100
100
  };
101
101
  }
102
+ if (f.name === "role_id") {
103
+ f.fieldview = "role_select";
104
+ await f.fill_fkey_options();
105
+ }
102
106
  }
103
107
  return userFields;
104
108
  };
@@ -110,65 +114,59 @@ const getUserFields = async (req) => {
110
114
  * @param {User} user
111
115
  * @returns {Promise<Form>}
112
116
  */
113
- const userForm = contract(
114
- is.fun(
115
- [is.obj({}), is.maybe(is.class("User"))],
116
- is.promise(is.class("Form"))
117
- ),
118
- async (req, user) => {
119
- const roleField = new Field({
120
- label: req.__("Role"),
121
- name: "role_id",
122
- type: "Key",
123
- reftable_name: "roles",
124
- });
125
- const roles = (await User.get_roles()).filter((r) => r.role !== "public");
126
- roleField.options = roles.map((r) => ({ label: r.role, value: r.id }));
127
- const can_reset = getState().getConfig("smtp_host", "") !== "";
128
- const userFields = await getUserFields(req);
129
- const form = new Form({
130
- fields: [roleField, ...userFields],
131
- action: "/useradmin/save",
132
- submitLabel: user ? req.__("Save") : req.__("Create"),
133
- });
134
- if (!user) {
117
+ const userForm = async (req, user) => {
118
+ const roleField = new Field({
119
+ label: req.__("Role"),
120
+ name: "role_id",
121
+ type: "Key",
122
+ reftable_name: "roles",
123
+ });
124
+ const roles = (await User.get_roles()).filter((r) => r.role !== "public");
125
+ roleField.options = roles.map((r) => ({ label: r.role, value: r.id }));
126
+ const can_reset = getState().getConfig("smtp_host", "") !== "";
127
+ const userFields = await getUserFields(req);
128
+ const form = new Form({
129
+ fields: userFields,
130
+ action: "/useradmin/save",
131
+ submitLabel: user ? req.__("Save") : req.__("Create"),
132
+ });
133
+ if (!user) {
134
+ form.fields.push(
135
+ new Field({
136
+ label: req.__("Set random password"),
137
+ name: "rnd_password",
138
+ type: "Bool",
139
+ default: true,
140
+ })
141
+ );
142
+ form.fields.push(
143
+ new Field({
144
+ label: req.__("Password"),
145
+ name: "password",
146
+ input_type: "password",
147
+ showIf: { rnd_password: false },
148
+ })
149
+ );
150
+ can_reset &&
135
151
  form.fields.push(
136
152
  new Field({
137
- label: req.__("Set random password"),
138
- name: "rnd_password",
153
+ label: req.__("Send password reset email"),
154
+ name: "send_pwreset_email",
139
155
  type: "Bool",
140
156
  default: true,
157
+ showIf: { rnd_password: true },
141
158
  })
142
159
  );
143
- form.fields.push(
144
- new Field({
145
- label: req.__("Password"),
146
- name: "password",
147
- input_type: "password",
148
- showIf: { rnd_password: false },
149
- })
150
- );
151
- can_reset &&
152
- form.fields.push(
153
- new Field({
154
- label: req.__("Send password reset email"),
155
- name: "send_pwreset_email",
156
- type: "Bool",
157
- default: true,
158
- showIf: { rnd_password: true },
159
- })
160
- );
161
- }
162
- if (user) {
163
- form.hidden("id");
164
- form.values = user;
165
- delete form.values.password;
166
- } else {
167
- form.values.role_id = roles[roles.length - 1].id;
168
- }
169
- return form;
170
160
  }
171
- );
161
+ if (user) {
162
+ form.hidden("id");
163
+ form.values = user;
164
+ delete form.values.password;
165
+ } else {
166
+ form.values.role_id = roles[roles.length - 1].id;
167
+ }
168
+ return form;
169
+ };
172
170
 
173
171
  /**
174
172
  * Dropdown for User Info in left menu
@@ -390,6 +388,7 @@ const http_settings_form = async (req) =>
390
388
  "cross_domain_iframe",
391
389
  "body_limit",
392
390
  "url_encoded_limit",
391
+ ...(!db.isSQLite ? ["prune_session_interval"] : []),
393
392
  ],
394
393
  action: "/useradmin/http",
395
394
  submitLabel: req.__("Save"),
package/auth/roleadmin.js CHANGED
@@ -145,7 +145,9 @@ router.get(
145
145
  active_sub: "Roles",
146
146
  contents: {
147
147
  type: "card",
148
- title: req.__("Roles"),
148
+ title:
149
+ req.__("Roles") +
150
+ `<a href="javascript:ajax_modal('/admin/help/User%20roles?')"><i class="fas fa-question-circle ms-1"></i></a>`,
149
151
  contents: [
150
152
  mkTable(
151
153
  [
@@ -198,7 +200,9 @@ router.get(
198
200
  sub2_page: "New",
199
201
  contents: {
200
202
  type: "card",
201
- title: req.__("Roles"),
203
+ title:
204
+ req.__("Roles") +
205
+ `<a href="javascript:ajax_modal('/admin/help/User%20roles?')"><i class="fas fa-question-circle ms-1"></i></a>`,
202
206
  contents: [renderForm(form, req.csrfToken())],
203
207
  },
204
208
  });
package/auth/routes.js CHANGED
@@ -1103,15 +1103,20 @@ router.post(
1103
1103
  res.redirect("/auth/twofa/login/totp");
1104
1104
  return;
1105
1105
  }
1106
+ let maxAge = null;
1106
1107
  if (req.session.cookie)
1107
1108
  if (req.body.remember) {
1108
- const setDur = +getState().getConfig("cookie_duration_remember", 0);
1109
- if (setDur) req.session.cookie.maxAge = setDur * 60 * 60 * 1000;
1110
- else req.session.cookie.expires = false;
1109
+ const setDur = +getState().getConfig("cookie_duration_remember", 720);
1110
+ if (setDur) {
1111
+ maxAge = setDur * 60 * 60 * 1000;
1112
+ req.session.cookie.maxAge = maxAge;
1113
+ } else req.session.cookie.expires = false;
1111
1114
  } else {
1112
- const setDur = +getState().getConfig("cookie_duration", 0);
1113
- if (setDur) req.session.cookie.maxAge = setDur * 60 * 60 * 1000;
1114
- else req.session.cookie.expires = false;
1115
+ const setDur = +getState().getConfig("cookie_duration", 720);
1116
+ if (setDur) {
1117
+ maxAge = setDur * 60 * 60 * 1000;
1118
+ req.session.cookie.maxAge = maxAge;
1119
+ } else req.session.cookie.expires = false;
1115
1120
  }
1116
1121
  const session_id = getSessionId(req);
1117
1122
 
@@ -1119,7 +1124,7 @@ router.post(
1119
1124
  session_id,
1120
1125
  old_session_id: req.old_session_id,
1121
1126
  });
1122
- res?.cookie?.("loggedin", "true");
1127
+ res?.cookie?.("loggedin", "true", maxAge ? { maxAge } : undefined);
1123
1128
  req.flash("success", req.__("Welcome, %s!", req.user.email));
1124
1129
  if (req.smr) {
1125
1130
  const dbUser = await User.findOne({ id: req.user.id });
@@ -1155,7 +1160,11 @@ router.get(
1155
1160
  } else {
1156
1161
  const auth = getState().auth_methods[method];
1157
1162
  if (auth) {
1158
- passport.authenticate(method, auth.parameters)(req, res, next);
1163
+ const passportParams =
1164
+ typeof auth.parameters === "function"
1165
+ ? auth.parameters(req)
1166
+ : auth.parameters;
1167
+ passport.authenticate(method, passportParams)(req, res, next);
1159
1168
  } else {
1160
1169
  req.flash(
1161
1170
  "danger",
@@ -1189,7 +1198,11 @@ router.post(
1189
1198
  const { method } = req.params;
1190
1199
  const auth = getState().auth_methods[method];
1191
1200
  if (auth) {
1192
- passport.authenticate(method, auth.parameters)(
1201
+ const passportParams =
1202
+ typeof auth.parameters === "function"
1203
+ ? auth.parameters(req)
1204
+ : auth.parameters;
1205
+ passport.authenticate(method, passportParams)(
1193
1206
  req,
1194
1207
  res,
1195
1208
  loginCallback(req, res)
@@ -1227,7 +1240,12 @@ const callbackFn = async (req, res, next) => {
1227
1240
  const { method } = req.params;
1228
1241
  const auth = getState().auth_methods[method];
1229
1242
  if (auth) {
1230
- passport.authenticate(method, { failureRedirect: "/auth/login" })(
1243
+ const passportParams =
1244
+ typeof auth.parameters === "function"
1245
+ ? auth.parameters(req)
1246
+ : auth.parameters;
1247
+ passportParams.failureRedirect = "/auth/login";
1248
+ passport.authenticate(method, passportParams)(
1231
1249
  req,
1232
1250
  res,
1233
1251
  loginCallback(req, res)
package/auth/testhelp.js CHANGED
@@ -9,6 +9,8 @@ const app = require("../app");
9
9
  const getApp = require("../app");
10
10
  const fixtures = require("@saltcorn/data/db/fixtures");
11
11
  const reset = require("@saltcorn/data/db/reset_schema");
12
+ const jsdom = require("jsdom");
13
+ const { JSDOM, ResourceLoader } = jsdom;
12
14
 
13
15
  /**
14
16
  *
@@ -307,6 +309,89 @@ const notFound = (res) => {
307
309
  }
308
310
  };
309
311
 
312
+ const load_url_dom = async (url) => {
313
+ const app = await getApp({ disableCsrf: true });
314
+ class CustomResourceLoader extends ResourceLoader {
315
+ async fetch(url, options) {
316
+ const url1 = url.replace("http://localhost", "");
317
+ //console.log("fetching", url, url1);
318
+ const res = await request(app).get(url1);
319
+
320
+ return Buffer.from(res.text);
321
+ }
322
+ }
323
+ const reqres = await request(app).get(url);
324
+ //console.log("rr1", reqres.text);
325
+ const virtualConsole = new jsdom.VirtualConsole();
326
+ virtualConsole.sendTo(console);
327
+ const dom = new JSDOM(reqres.text, {
328
+ url: "http://localhost" + url,
329
+ runScripts: "dangerously",
330
+ resources: new CustomResourceLoader(),
331
+ pretendToBeVisual: true,
332
+ virtualConsole,
333
+ });
334
+
335
+ class FakeXHR {
336
+ constructor() {
337
+ this.readyState = 0;
338
+ this.requestHeaders = [];
339
+ //return traceMethodCalls(this);
340
+ }
341
+ open(method, url) {
342
+ //console.log("open xhr", method, url);
343
+ this.method = method;
344
+ this.url = url;
345
+ }
346
+
347
+ addEventListener(ev, reqListener) {
348
+ if (ev === "load") this.reqListener = reqListener;
349
+ }
350
+ setRequestHeader(k, v) {
351
+ this.requestHeaders.push([k, v]);
352
+ }
353
+ overrideMimeType() {}
354
+ async send(body) {
355
+ //console.log("send1", this.url);
356
+ const url1 = this.url.replace("http://localhost", "");
357
+ //console.log("xhr fetching", url1);
358
+ let req =
359
+ this.method == "POST"
360
+ ? request(app).post(url1)
361
+ : request(app).get(url1);
362
+ for (const [k, v] of this.requestHeaders) {
363
+ req = req.set(k, v);
364
+ }
365
+ if (this.method === "POST" && body) req.send(body);
366
+ const res = await req;
367
+ this.responseHeaders = res.headers;
368
+ if (res.headers["content-type"].includes("json"))
369
+ this.responseType = "json";
370
+ this.response = res.text;
371
+ this.responseText = res.text;
372
+ this.status = res.status;
373
+ this.statusText = "OK";
374
+ this.readyState = 4;
375
+ if (this.reqListener) this.reqListener(res.text);
376
+ if (this.onload) this.onload(res.text);
377
+ //console.log("agent res", res);
378
+ //console.log("xhr", this);
379
+ }
380
+ getAllResponseHeaders() {
381
+ return Object.entries(this.responseHeaders)
382
+ .map(([k, v]) => `${k}: ${v}`)
383
+ .join("\n");
384
+ }
385
+ }
386
+ dom.window.XMLHttpRequest = FakeXHR;
387
+ await new Promise(function (resolve, reject) {
388
+ dom.window.addEventListener("DOMContentLoaded", (event) => {
389
+ resolve();
390
+ });
391
+ });
392
+ return dom;
393
+ };
394
+
310
395
  module.exports = {
311
396
  getStaffLoginCookie,
312
397
  getAdminLoginCookie,
@@ -328,4 +413,5 @@ module.exports = {
328
413
  succeedJsonWithWholeBody,
329
414
  resToLoginCookie,
330
415
  itShouldIncludeTextForAdmin,
416
+ load_url_dom,
331
417
  };
@@ -0,0 +1,11 @@
1
+ A table holds data organised by rows and columns, which you can visualise in a
2
+ two-dimensional grid. The rows hold the different cases, about which you have similar data.
3
+ Each column is called a Field, and every field must have a label (and a type).
4
+
5
+ The field label allows you to recognize the meaning of the values in the column in a
6
+ human-readable name. In a spreadsheet with a lot of data, it is often used as the header for
7
+ each column.
8
+
9
+ In Saltcorn, you can use spaces in field labels. Because we also need a valid identifier to use
10
+ in formulae, a variable name is generated from the label by replacing spaces with underscores and
11
+ converting all uppercase characters to lowercase.
@@ -0,0 +1,39 @@
1
+ Every field in a table stores values of a specific type. The type limits which
2
+ values this field can take in each row. For instance, if a field has a type `Integer`
3
+ then it cannot take the value `"Simon"` but the value `17` is admissible.
4
+
5
+ When building your table, you must therefore think carefully about not only which fields
6
+ to include but also what the types are. Sometimes this is easy - you know that names
7
+ should be stored as as `String` type. But at other times you need to think ahead a bit
8
+ and think carefully about what data your table will hold
9
+
10
+ Some types are more general than others. The `String` type and the `JSON`
11
+ type (from the json module) can hold values that can be represented by more specific types.
12
+ For instance the number 17 can be stored in a string field where it will be represented by the
13
+ string `"17"`. You should always try to use the most specific type available. This will give
14
+ you access to user interface elements that are richer and more appropriate for the data in the
15
+ given field. For example, you could use `String` to store dates, but then you will not
16
+ be able to use an interactive date picker or to display dates in flexible formats.
17
+
18
+ Most field types have some additional parameters that will be configured on the next screen.
19
+ `Integer` types have minimum and maximum values and strings can be restricted to a set of
20
+ options or by a regular expression. This allows you to further narrow what data it is admissible
21
+ in your table.
22
+
23
+ These types are available in your current installation:
24
+
25
+ {{# const tys = Object.values(scState.types) }}
26
+ {{# for (const ty of tys) { }}
27
+ * {{ ty.name }}{{ty.description ? `: ${ty.description}` : ""}}
28
+ {{# } }}
29
+
30
+ Further types can be installed from moduels in the [Module store](/plugins)
31
+
32
+ In addition, a field can have types File and Key to a table.
33
+
34
+ A File field holds a reference to a file in the Saltcorn file system. This is stored by the,
35
+ path to the file and if the file moves, the reference may become invalid.
36
+
37
+ A field that is a Key to another table is a reference to a row in that table. This is also known
38
+ as a foreign key and is used to link data between tables and to create the relational structure
39
+ of your application.
@@ -0,0 +1,38 @@
1
+ {{# const srcTable = Table.findOne({ name: query.table_name }) }}
2
+
3
+ The row inclusion formula allows you to put further restrictions on the rows that are
4
+ included in the list or feed view. The restrictions from this formula are on top
5
+ of the filter state (from the URL, set by filters), so to be included a row must satisfy
6
+ both the row inclusion Formula and the filter state.
7
+
8
+ The row inclusion formula is a JavaScript expression that must evaluate to a boolean. Typically
9
+ this involves an equality (`==`) or inequality (`<` or `>`) operator.
10
+
11
+ In scope is the variables from your table accessed by their variable name. The fields in the current
12
+ table are:
13
+
14
+ | Field | Variable name | Type |
15
+ | ----- | ------------- | ---- |
16
+ {{# for (const field of srcTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
17
+ {{# } }}
18
+
19
+ In addition, you can use the function `today` to compare a Date field against a specific point in time. Depending
20
+ on its argument, `today` will return different date values:
21
+
22
+ * `today()` returns the current time
23
+ * `today(x)` where `x` is a positive number returns the time `x` days in the future. Example: `today(1)` is this time, tomorrow.
24
+ * `today(x)` where `x` is a negative number returns the time `x` days ago. Example: `today(-1)` is this time, yesterday.
25
+ * `today({startOf: unit})` where `unit` is a string, one of: "year", "month", "quarter", "week", "day", "hour",
26
+ "minute" and "second", returns the time at the beginning of the current given time period. Example:
27
+ `today({startOf: "month"})` is the start of the current month.
28
+ * `today({endOf: unit})` is similar to `today({startOf: unit})` but is the end of the given time unit. Example:
29
+ `today({endOf: "week"})` is the end of the current week.
30
+
31
+ ### Example
32
+
33
+ If you have a tasks list with a boolean field `done` and a date field `due`, to display all fields where done is `false`
34
+ and are due this week:
35
+
36
+ `done == false &&
37
+ due > today({startOf: "week"}) &&
38
+ due < today({endOf: "week"})`
@@ -0,0 +1,76 @@
1
+ The ability to read or write to tables is normally limited by the settings
2
+ for "Minimum role to read" and "Minimum role to write", which is compared
3
+ to the user's role.
4
+
5
+ In some cases you want some users wo be able to read and write to some but
6
+ not to all rows. Some examples are:
7
+
8
+ * On a blog, Users should be able to create comments and to edit comments they have made.
9
+ But you do not want users to be able to edit another user's comments.
10
+
11
+ * In a todo list, Users should be able to create and edit new items for themselves but
12
+ they should not be able to read or edit items for other users.
13
+
14
+ * In a project management app, you may only want you supposed to be able to see and
15
+ contribute to projects they have been assigned to.
16
+
17
+ Saltcorn contains an authorization system that can be very simple (limit everything by role),
18
+ more flexible (rows have a user field and if you are that user, you can edit a row)
19
+ to very complex (featuring many-to-many relationships, where the user field can be on a
20
+ different table; and inheritance, where authorization schemes propagate through relationship).
21
+
22
+ ### Role-based authorization
23
+
24
+ In Saltcorn, every user has a role and the roles have a strictly hierachical ordering,
25
+ which you [can edit](/roleadmin). By
26
+ using the "Minimum role to read" and "Minimum role to write" settings for the table, you
27
+ can create a role cutoff limit for access. See the help topics for those settings for details.
28
+
29
+ ### Simple user field ownership
30
+
31
+ In the simplest deviation from role-based authorization, you can grant access to edit
32
+ a row to users that match a Key to users field on the row. To use this, you should:
33
+
34
+ 1. Set the "Minimum role to read" and "Minimum role to write" to a role that would stop the
35
+ user from accessing the row.
36
+
37
+ 2. Create a field with type Key to users. This field should ideally not be labelled `User` or `user`,
38
+ as this variable name will clash with access to logged-in user object in formulae.
39
+
40
+ 3. Make sure this is filled in when the user creates the row.
41
+ For instance in an Edit view under the "Fixed and blocked fields" settings, in the
42
+ Preset for this field pick the LoggedIn preset.
43
+
44
+ 4. Pick this field as the Ownership field from the dropdown in the table settings.
45
+
46
+ This is an additional access grant
47
+ in addition to that given by the minimum roles to read and write. If your user does *not*
48
+ match the designated field, The decision to grant access reverts to the role-based settings.
49
+ You therefore cannot use ownership to limit access, only to grant additional access.
50
+
51
+ ### Authorization by inheritance
52
+
53
+ If the table has a relationship (that is, has a field with Key type) with another table which
54
+ has an ownership field (or ownership formula), it can instead inherit the onwership from that
55
+ table - essentially, take the ownership of the row the Key is pointing to.
56
+
57
+ For instance, you have a project management app with a Projects table that has an `owner` Key to users
58
+ field and this is set as the ownership field; and a Tasks table with a Key to Projects field.
59
+ In this case "Inherit Projects" is available as an option in the Ownership field dropdown.
60
+ If you pick it and reload the page, you will See that it is in fact implemented as an ownership
61
+ formula which is created for you.
62
+
63
+ ### Authorization by user groups
64
+
65
+ If you need to grant access to tables based not on user fields on this table (ownership field)
66
+ or on tables it has keys to (inheritance), you can declare a table to be a user group.
67
+ The user group should be a table that has a key to users, and may also have a key to this table.
68
+ Use this to grant ownership rights to a row to more than one user. For instance, if more than one
69
+ user is working on a project, you can declare that all users assigned to this project are owners. See
70
+ the help topic for the User group option.
71
+
72
+ ### Authorisation by formula
73
+
74
+ You can also grant access to edit the row if an arbitrary formula is true. This can be used
75
+ For very flexible authorisation schemes. Choose formula in the ownership field drop down and then see
76
+ help for the Ownership formula.
@@ -0,0 +1,75 @@
1
+ An ownership formula on a table allows you to develop an extremely flexible
2
+ Authorisation scheme. It is also the mechanism by which ownership inheritance
3
+ and ownership by user groups is implemented. If you pick one of these options
4
+ in the Ownership field drop-down it will essentially generate the corresponding
5
+ formula for you.
6
+
7
+ The ownership formula is a JavaScript expression which should evaluate to a boolean
8
+ (true/false) value. If it evaluate to true (or a [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) value), then the logged-in user is an owner and
9
+ can read and write the row. If it evaluate to false (or a [falsey](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) value),
10
+ the user is not the owner. However, If they have a role equal or higher than that
11
+ required to read or write, they can still perform this operation. You cannot use the
12
+ ownership formula to deny access to a user who satisfies the role condition.
13
+
14
+ When evaluating the formula, the values in scope are as follows:
15
+
16
+ #### Row fields
17
+
18
+ All field values for the row being evaluated are in scope and can be addressed by
19
+ their variable name.
20
+
21
+ #### Join fields on row keys
22
+
23
+ For any fields on the current table that are Keys to another table, you can access the values
24
+ in the linked table by using the dot notation. For instance, if you have a field labelled
25
+ "Project" (variable name `project`) which is of type Key to Projects, and the Projects
26
+ table has a `name` field, you can refer to this as `project.name`. However, if the Project field
27
+ is not required, this can trigger an error as accessing a subfield on a `null` variable is a
28
+ JavaScript error. In that case, you can use [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)
29
+ (`project?.name`) which will evaluate to `null` if `project` is `null`.
30
+
31
+ #### User object
32
+
33
+ You can refer to the logged in user using the variable name user.
34
+ This is an object and you must use the dot to access user fields,
35
+ e.g. user.id for the logged in users id.
36
+
37
+ The user object has other fields than the primary key identifier.
38
+ For your login session the user object is:
39
+
40
+ ```
41
+ {{JSON.stringify(user, null, 2)}}
42
+ ```
43
+
44
+ #### User groups
45
+
46
+ If any tables have been designated as user groups (that is, if they User group option has been
47
+ checked in the table settings) then a value will appear in the user object, which is the list
48
+ of an array of user group rows to which the user belongs. The name of that field is
49
+ `{table name}_by_{user field in user group}`.
50
+
51
+ Normally you don't have to write theownership formula, you select it from the Ownership field
52
+ drop-down and the formula is generated for you.
53
+
54
+ When changes are made to user group membership, the user needs to login and log out again before these
55
+ changes are reflected in the user object. If you are removing user group membership you may need to force
56
+ log out those users.
57
+
58
+ ## Examples
59
+
60
+ User table has a `String` field named `department` which can take options "Finance", "HR",
61
+ or "Warp drive engineering". To give ownership to the Expenses table to users in the Finance
62
+ department:
63
+
64
+ `user.department === "Finance"`
65
+
66
+ Same as above, but the Expenses table also has a `filled_by` field which is Key to user, and
67
+ you want to give ownership to that user or any user in the Finance department:
68
+
69
+ `user.department === "Finance" || user.id === filled_by`
70
+
71
+ Similar to above, however department membership is not stored as User field but as in a table
72
+ named "User In Department" with a Key to user field labelled "Employee" and a string field "Name" for
73
+ the department name.
74
+
75
+ `user.UserinDepartment_by_employee.map(d=>d.name).includes("Finance") || user.id === filled_by`
@@ -0,0 +1,20 @@
1
+ You can restrict which users can read or write to this table by their role.
2
+
3
+ Each user in Saltcorn has a role and the roles have a strictly hierachical ordering,
4
+ which you [can edit](/roleadmin). The ordering means that users in a role can access
5
+ everything the users in the role "below" then can acceess, but the users in the role
6
+ "above" have further access.
7
+
8
+ Assigning access by role is a quick way to give users more or less access based on how
9
+ much you trust them.
10
+
11
+ Using the settings for "Minimum role to read" and "Minimum role to write" you set the roles
12
+ required to read and write to the table, respectively. Users also need to have the roles
13
+ required for running views and pages.
14
+
15
+ Restricting table access by role is the simplest form of authorisation in Saltcorn,
16
+ but it is often too limited. Row ownership is much more flexible; see the help topic for
17
+ Ownership field.
18
+
19
+ Note that if the user has ownership of the row, they can read and write that row even if
20
+ they have a role below the minimum role to read and write, respectively.
@@ -0,0 +1,35 @@
1
+ User groups are used to grant access to tables based on membership of groups
2
+ where each user can be a member of many groups and each group can have many
3
+ users.
4
+
5
+ Any table that has a field of type Key to User can be designated a user group, by
6
+ checking the User group option. This means that when a User group table has a row with
7
+ a key to a user, that user is a member of a group.
8
+
9
+ The consequence of designating a table as a User group is that if there is also a Key from
10
+ the user group table to another table, then the option of group membership appears in the
11
+ drop-down for the Ownership field option.
12
+
13
+ In addition, If a table is designated as a user group. a value indicating group membership
14
+ appears in the user object. For this to appear, the variable has to be referenced in an
15
+ ownership formula. The name of this variable is
16
+ `{user group table name}_by_{key to user field name}`.
17
+
18
+ When changes are made to user group membership, the user needs to login and log out again before these
19
+ changes are reflected in the user object. If you are removing user group membership you may need to force
20
+ log out those users.
21
+
22
+ ### Example
23
+
24
+ In a project management application you have a "Projects" table and a "Tasks" table (with a
25
+ Key to project). Several people can work on the same project.
26
+
27
+ You would like to restrict access to the project table such that only uses to work on the project have access.
28
+
29
+ 1. Create a "User Works On Project" table with a Key to projects field and a Key to user field called Participant.
30
+
31
+ 2. Designate the "User Works On Project" table as a user group by checking the user group box.
32
+
33
+ 3. In the project table settings, In the ownership field, there is now an option called "In User Works On Project by Participant", pick this option.
34
+
35
+ 4. Now add rows to the "User Works On Project" table. When the users logout and login again they will have the required access.