Commit c831dabc authored by Emmanuel Raviart's avatar Emmanuel Raviart
Browse files

Authenticate users using Zammad instead of PostgreSQL data wrapper.

parent 4951d2fa
......@@ -47,62 +47,6 @@ As normal user, install dependencies:
npm install
```
### Adding (mostly read-only) Account for Data-Catalogue to Prodedo existing MySQL Database
On Progedo MySQL database server:
```bash
mysql quetelet_commande
CREATE USER 'data_catalogue'@'localhost' IDENTIFIED BY 'data_catalogue';
GRANT SELECT ON diffuseur TO 'data_catalogue'@'localhost';
GRANT SELECT ON discipline TO 'data_catalogue'@'localhost';
GRANT SELECT ON pays TO 'data_catalogue'@'localhost';
GRANT INSERT, SELECT, UPDATE ON profil TO 'data_catalogue'@'localhost';
GRANT SELECT ON role TO 'data_catalogue'@'localhost';
GRANT SELECT ON statut TO 'data_catalogue'@'localhost';
GRANT INSERT, SELECT, UPDATE ON utilisateur TO 'data_catalogue'@'localhost';
GRANT INSERT, SELECT, UPDATE ON utilisateur_option TO 'data_catalogue'@'localhost';
FLUSH PRIVILEGES;
exit
```
#### Using _Debian GNU/Linux_
```bash
apt install postgresql-13-mysql-fdw
su - postgres
psql data_catalogue
```
#### Using _MacOS_
First, install [MySQL Foreign Data Wrapper for PostgreSQL](https://github.com/EnterpriseDB/mysql_fdw), then:
```bash
psql data_catalogue
```
#### For everybody
```sql
CREATE EXTENSION IF NOT EXISTS mysql_fdw;
CREATE SERVER progedo_mysql_server
FOREIGN DATA WRAPPER mysql_fdw
OPTIONS (host '127.0.0.1', port '3306');
GRANT USAGE ON FOREIGN SERVER progedo_mysql_server TO postgres;
CREATE USER MAPPING FOR postgres
SERVER progedo_mysql_server
OPTIONS (username 'data_catalogue', password 'data_catalogue');
GRANT USAGE ON FOREIGN SERVER progedo_mysql_server TO data_catalogue;
CREATE USER MAPPING FOR data_catalogue
SERVER progedo_mysql_server
OPTIONS (username 'data_catalogue', password 'data_catalogue');
exit
```
As normal user, create database tables:
```bash
......
......@@ -12,9 +12,6 @@ DB_PASSWORD="data_catalogue"
DEV_AUTHENTICATION='{"email": "john.doe@example.com", "family_name": "Doe", "given_name": "John"}'
# MySQL database configuration
MYSQL_DB_NAME="quetelet_commande"
# OpenID Connect configuration
# OPENID_CONNECT_CLIENT_ID="OPENID_CONNECT_CLIENT_ID"
# OPENID_CONNECT_CLIENT_SECRET="OPENID_CONNECT_CLIENT_SECRET"
......@@ -38,3 +35,9 @@ SESSION_SECRET="SESSION_SECRET"
TEXTS_DIR=https://git.nomics.world/progedo/data-catalogue-textes/-/raw/master/
TITLE="Réseau Quételet (dev)"
# Token to use for API of Zammad server
ZAMMAD_TOKEN="a secret token that must not be shared"
# URL of Zammad server
ZAMMAD_URL="https://support.example.com/"
......@@ -53,15 +53,6 @@ export function auditConfig(audit: Audit, data: any): [any, any] {
auditTrimString,
auditJson5,
)
audit.attribute(
data,
"mySqlDb",
true,
errors,
remainingKeys,
auditMySqlDb,
auditRequire,
)
audit.attribute(
data,
"openIdConnect",
......@@ -92,6 +83,15 @@ export function auditConfig(audit: Audit, data: any): [any, any] {
auditRequire,
)
}
audit.attribute(
data,
"zammad",
true,
errors,
remainingKeys,
auditZammad,
auditRequire,
)
return audit.reduceRemaining(data, errors, remainingKeys)
}
......@@ -138,31 +138,6 @@ export function auditDb(audit: Audit, data: any): [any, any] {
return audit.reduceRemaining(data, errors, remainingKeys)
}
export function auditMySqlDb(audit: Audit, data: any): [any, any] {
if (data == null) {
return [data, null]
}
if (typeof data !== "object") {
return audit.unexpectedType(data, "object")
}
data = { ...data }
const errors: { [key: string]: any } = {}
const remainingKeys = new Set(Object.keys(data))
audit.attribute(
data,
"database",
true,
errors,
remainingKeys,
auditTrimString,
auditRequire,
)
return audit.reduceRemaining(data, errors, remainingKeys)
}
function auditOpenIdConnect(audit: Audit, data: any): [any, any] {
if (data == null) {
return [data, null]
......@@ -209,6 +184,40 @@ function auditOpenIdConnect(audit: Audit, data: any): [any, any] {
return audit.reduceRemaining(data, errors, remainingKeys)
}
export function auditZammad(audit: Audit, data: any): [any, any] {
if (data == null) {
return [data, null]
}
if (typeof data !== "object") {
return audit.unexpectedType(data, "object")
}
data = { ...data }
const errors: { [key: string]: any } = {}
const remainingKeys = new Set(Object.keys(data))
audit.attribute(
data,
"token",
true,
errors,
remainingKeys,
auditTrimString,
auditRequire,
)
audit.attribute(
data,
"url",
true,
errors,
remainingKeys,
auditHttpUrl,
auditRequire,
)
return audit.reduceRemaining(data, errors, remainingKeys)
}
export function validateConfig(data: any): [any, any] {
return auditConfig(cleanAudit, data)
}
<script lang="ts">
import { stores } from "@sapper/app"
import type { Profil } from "../progedo/data"
import type { ZammadUser } from "../zammad"
const { session } = stores()
$: user = $session.user as Profil | undefined
$: user = $session.user as ZammadUser | undefined
$: role = user?.utilisateur?.role
$: roles = user?.roles
</script>
{#if user == null}
......@@ -21,10 +21,15 @@
<strong>Access restricted!</strong>
Your credentials don't allow you to access to this page.
</p>
{#if role == null}
{#if roles == null}
<i class="italic">You don't have any role.</i>
{:else}
<p>Your role: {role.nom}</p>
<p>Your roles:</p>
<ul class="list-disc list-inside">
{#each roles as role}
<li>{role}</li>
{/each}
</ul>
{/if}
</div>
{/if}
......@@ -2,8 +2,8 @@
import { goto, stores } from "@sapper/app"
import { validateJsonResponse } from "../auditors/responses"
import { proposedLanguagesAndLabels } from "../locales"
import type { Profil } from "../progedo/data"
import { debugMode, language, localize, shoppingCart } from "../stores"
import type { ZammadUser } from "../zammad"
export let display: string
export let open: boolean
......@@ -22,9 +22,9 @@
new URL($page.path, $session.baseUrl).toString(),
)}`
$: user = $session.user as Profil | undefined
$: user = $session.user as ZammadUser | undefined
$: role = user?.utilisateur?.role
$: roles = user?.roles
async function login() {
open = false
......@@ -171,9 +171,9 @@
>
<span
class="mr-1"
title="{user.prenom} {user.nom} <{user.email}> {role == null
? ''
: `(${role.nom})`} ">{user.prenom}</span
title="{user.firstname} {user.lastname} <{user.email}{roles?.length
? ' ' + roles.join(', ')
: ''}>">{user.firstname}</span
>
</button>
{#if userMenuOpen}
......
......@@ -8,8 +8,8 @@
import { validateJsonResponse } from "../auditors/responses"
import OrganizationName from "./OrganizationName.svelte"
import type { Study } from "../data"
import type { Profil } from "../progedo/data"
import { shoppingCart } from "../stores"
import type { ZammadUser } from "../zammad"
export let study: Study
......@@ -21,7 +21,7 @@
$: topics = [...iterStudyTopicsClass(study)]
$: user = $session.user as Profil | undefined
$: user = $session.user as ZammadUser | undefined
async function toggleCart() {
let cart = $shoppingCart
......@@ -37,7 +37,7 @@
localStorage.setItem("shoppingCart", JSON.stringify(cart))
}
if (user !== undefined) {
const response = await fetch("api/cart", {
const response = await fetch("api/carts", {
body: JSON.stringify({ paths: cart }, null, 2),
credentials: "include",
headers: {
......
......@@ -8,8 +8,8 @@
import { validateJsonResponse } from "../auditors/responses"
//import OrganizationName from "./OrganizationName.svelte"
import type { Study } from "../data"
import type { Profil } from "../progedo/data"
import { shoppingCart } from "../stores"
import type { ZammadUser } from "../zammad"
export let study: Study
......@@ -19,7 +19,7 @@
// $: producers = [...iterStudyProducers(study)]
// $: topics = [...iterStudyTopicsClass(study)]
$: user = $session.user as Profil | undefined
$: user = $session.user as ZammadUser | undefined
async function toggleCart() {
let cart = $shoppingCart
......@@ -35,7 +35,7 @@
localStorage.setItem("shoppingCart", JSON.stringify(cart))
}
if (user !== undefined) {
const response = await fetch("api/cart", {
const response = await fetch("api/carts", {
body: JSON.stringify({ paths: cart }, null, 2),
credentials: "include",
headers: {
......
......@@ -26,11 +26,11 @@
import VariableBody from "./VariableBody.svelte"
import DatafileBody from "./DatafileBody.svelte"
import type { Study } from "../data"
import type { Profil } from "../progedo/data"
import { localize, shoppingCart } from "../stores"
import { slugify } from "../strings"
import type { Tab } from "../tabs"
import { ensureValidTab } from "../tabs"
import type { ZammadUser } from "../zammad"
export let study: Study
......@@ -54,7 +54,7 @@
$: tab = ensureValidTab($page.query, tabs)
$: user = $session.user as Profil | undefined
$: user = $session.user as ZammadUser | undefined
// CodeBook Fields
$: codeBook = study.codeBook!
......@@ -123,7 +123,7 @@
localStorage.setItem("shoppingCart", JSON.stringify(cart))
}
if (user !== undefined) {
const response = await fetch("api/cart", {
const response = await fetch("api/carts", {
body: JSON.stringify({ paths: cart }, null, 2),
credentials: "include",
headers: {
......
......@@ -12,9 +12,6 @@ const config = {
password: process.env.DB_PASSWORD,
},
devAuthentication: process.env.DEV_AUTHENTICATION,
mySqlDb: {
database: process.env.MYSQL_DB_NAME,
},
openIdConnect: process.env.OPENID_CONNECT_CLIENT_ID
? {
clientId: process.env.OPENID_CONNECT_CLIENT_ID,
......@@ -31,6 +28,10 @@ const config = {
"https://git.nomics.world/progedo/data-catalogue-textes/-/raw/master/",
title: process.env.TITLE || "Data Catalogue",
// webSocketBaseUrl is defined below.
zammad: {
token: process.env.ZAMMAD_TOKEN,
url: process.env.ZAMMAD_URL,
},
}
const [validConfig, error] = validateConfig(config)
......
......@@ -3,7 +3,6 @@ import dedent from "dedent-js"
import { Language, postgreSqlConfigurationNameByLanguage } from "./data"
import { db, versionNumber } from "./database"
import { configureProgedoForeignDataWrapper } from "./progedo/configure"
export async function configure(): Promise<void> {
await configureDatabase()
......@@ -67,18 +66,28 @@ async function configureDatabase(): Promise<void> {
// Tables
await configureProgedoForeignDataWrapper()
// Table: carts
await db.none(
dedent`
CREATE TABLE IF NOT EXISTS carts (
id bigint NOT NULL PRIMARY KEY, -- REFERENCES utilisateur(id) ON DELETE CASCADE
id bigint NOT NULL PRIMARY KEY, -- user ID
paths text[] NOT NULL
)
`,
)
// Table: demands
await db.none(
dedent`
CREATE TABLE IF NOT EXISTS demands (
id bigserial PRIMARY KEY,
series_path text[],
studies_path text[],
user_id bigint NOT NULL
)
`,
)
// Table: languages
await db.none(
dedent`
......@@ -277,6 +286,16 @@ async function configureDatabase(): Promise<void> {
// Add comments once every table and column exists.
for (const command of [
"COMMENT ON TABLE carts IS 'shopping cart of each identified user'",
"COMMENT ON COLUMN carts.id IS 'Zammad user ID'",
"COMMENT ON COLUMN carts.paths IS 'paths of series and studies in cart'",
"COMMENT ON TABLE demands IS 'requests of series & studies made bu users'",
"COMMENT ON COLUMN demands.id IS 'unique ID of demand'",
"COMMENT ON COLUMN demands.series_path IS 'paths of requested series'",
"COMMENT ON COLUMN demands.studies_path IS 'paths of requested studies'",
"COMMENT ON COLUMN demands.user_id IS 'Zammad user ID'",
"COMMENT ON TABLE languages IS 'languages used by CodeBooks'",
"COMMENT ON COLUMN languages.code IS '2-letters ISO code of language'",
"COMMENT ON COLUMN languages.postgresql_configuration_name IS 'name of" +
......
......@@ -27,7 +27,7 @@ export const db = pgPromise({
user: config.db.user,
})
export let dbSharedConnectionObject: IConnected<{}, IClient> | null = null
export const versionNumber = 3
export const versionNumber = 4
/// Check that database exists and is up to date.
export async function connectDb(): Promise<void> {
......
import dedent from "dedent-js"
import config from "../config"
import { db } from "../database"
export async function configureProgedoForeignDataWrapper() {
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS diffuseur (
id bigint NOT NULL,
acronyme character varying(16) NOT NULL,
nom character varying(128) NOT NULL,
adresse text NOT NULL,
liste_distribution_csv text NOT NULL,
default_validator_id bigint,
autovalidation_ignore_other_university boolean DEFAULT false,
remote_file_ftp_host character varying(255) DEFAULT NULL::character varying,
remote_file_ftp_path character varying(255) DEFAULT NULL::character varying,
remote_file_ftp_login character varying(255) DEFAULT NULL::character varying,
remote_file_ftp_password character varying(255) DEFAULT NULL::character varying
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'diffuseur')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS discipline (
id bigint NOT NULL,
nom character varying(128) NOT NULL,
ordre bigint DEFAULT '0'::bigint NOT NULL
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'discipline')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS pays (
id bigint NOT NULL,
continent_id bigint NOT NULL,
nom character varying(128) NOT NULL,
cog character varying(5) NOT NULL
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'pays')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS profil (
id bigint NOT NULL,
utilisateur_id bigint NOT NULL,
complet boolean DEFAULT false NOT NULL,
version bigint NOT NULL,
email character varying(255) NOT NULL,
nom character varying(128) NOT NULL,
prenom character varying(128) NOT NULL,
telephone character varying(32) DEFAULT NULL::character varying,
date_creation timestamp with time zone,
statut_id bigint,
statut_autre character varying(255) DEFAULT NULL::character varying,
discipline_id bigint,
institution_id bigint,
institution_autre character varying(255) DEFAULT NULL::character varying,
entite_type_id bigint,
entite_nom character varying(255) DEFAULT NULL::character varying,
entite_code character varying(255) DEFAULT NULL::character varying,
entite_adresse_rue character varying(255) DEFAULT NULL::character varying,
entite_adresse_cp character varying(16) DEFAULT NULL::character varying,
entite_adresse_ville character varying(255) DEFAULT NULL::character varying,
entite_adresse_pays_id bigint,
entite_url character varying(512) DEFAULT NULL::character varying,
formation character varying(128) DEFAULT NULL::character varying,
resp_1_type_id bigint,
resp_1_nom character varying(128) DEFAULT NULL::character varying,
resp_1_prenom character varying(128) DEFAULT NULL::character varying,
resp_1_email character varying(255) DEFAULT NULL::character varying,
resp_2_type_id bigint,
resp_2_nom character varying(128) DEFAULT NULL::character varying,
resp_2_prenom character varying(128) DEFAULT NULL::character varying,
resp_2_email character varying(255) DEFAULT NULL::character varying
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'profil')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS role (
id bigint NOT NULL,
code character varying(32) NOT NULL,
nom character varying(64) NOT NULL
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'role')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS statut (
id bigint NOT NULL,
statut_type_id bigint NOT NULL,
nom character varying(64) DEFAULT NULL::character varying,
autre boolean DEFAULT false NOT NULL,
ordre bigint DEFAULT '0'::bigint NOT NULL
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'statut')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS utilisateur (
id bigint NOT NULL,
mot_de_passe text NOT NULL,
valide boolean DEFAULT false NOT NULL,
actif boolean DEFAULT false NOT NULL,
date_creation timestamp with time zone,
role_id bigint NOT NULL,
diffuseur_id bigint,
profil_principal_id bigint,
salt character varying(255) NOT NULL
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'utilisateur')
`,
{
dbname: config.mySqlDb.database,
},
)
await db.none(
dedent`
CREATE FOREIGN TABLE IF NOT EXISTS utilisateur_option (
id bigint NOT NULL,
utilisateur_id bigint NOT NULL,
code character varying(255) NOT NULL,
value text
)
SERVER progedo_mysql_server
OPTIONS (dbname $<dbname>, table_name 'utilisateur_option')
`,
{
dbname: config.mySqlDb.database,
},
)
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import type { ProgedoFollow, ProgedoJsonData, RetrievalError } from "./data"
import { fromProgedoJson } from "./data_consumers"
import { stringifyQuery } from "../urls"
import { validateJsonResponse } from "../auditors/responses"
export type ErrorHandler<T> = (
location: string,
url: string,
response: { status: number },
result: T,
error: any,
) => T