diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5043f..126bdb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Bump `triplestore` and `publication-triplestore` to `v1.3.0-rc.1`. [OP-2492] [OP-3547] - Add missing docker compose keys. [DL-6490] - Reorganize delta consumers config to harmonize with the ecosystem +- Set up the dashboard app [OP-3103] ### Deploy notes ``` drc restart migrations; drc logs -ft --tail=200 migrations @@ -20,6 +21,26 @@ drc up -d mandatarissen-consumer leidinggevenden-consumer worship-services-main- drc up -d search elasticsearch ``` +#### Dashboard + +The dashboard requires some server specific configuration before it can be used. + +##### 1. Dashboard domains +To configure the domain name that's used by the dashboard frontend, you can add it to the `VIRTUAL_HOST` and `LETSENCRYPT_HOST` environment variables of the identifier service. +- DEV domain: dev.dashboard.app-organisation-portal.contacthub-dev.s.redhost.be +- QA domain: qa.dashboard.app-organisation-portal.contacthub-dev.s.redhost.be +- PROD domain: dashboard.app-organisation-portal.organisaties.s.redpencil.io + +##### 2. Dashboard users +Before we can generate a user we need to add an application salt to the server. Add a `MU_APPLICATION_SALT` environment variable to the `dashboard-login` service in the docker-compose.override.yml file. + +Once this is done you can generate a user by running `mu script project-scripts generate-dashboard-login` and following the prompts. A new local migration will be generated which can be run by restarting the migrations service and which will insert the new user into the database. + +Store the login credentials as a comment in the docker-compose.override.yml file. + +##### 3. (Re)start the needed services +`drc up -d frontend-dashboard dashboard-login identifier; drc restart dispatcher resource db` + ## v1.30.5 (2025-03-10) ### Backend - Added `http://www.w3.org/ns/prov#Location` to delta stream 'public' [DL-6496] diff --git a/README.md b/README.md index 4fa218d..1884f32 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,26 @@ To make sure the app can share data, producers need to be set up. There is an in ) } ``` + +## Configuring the dashboard +### Accessing the dashboard from your local machine + +Since we use dispatcher v2, which dispatches on hostname, we'll have to update `/etc/hosts`. +Add an entry similar to the following. Ensure the first part of the domain starts with `dashboard`.: + +``` +127.0.0.1 dashboard.localhost +``` + +### Creating new users +For now, we use specific logins for the dashboard users. Each environment has its own passwords. + +To add a user, make sure to have [mu-cli](https://github.com/mu-semtech/mu-cli) installed first. +Then in `docker-compose.override.yml` +``` + dashboard-login: + environment: + MU_APPLICATION_SALT: 'a_random_string_with_sufficient_entropy_hence_not_this_one' +``` +You can now generate a user by running `mu script project-scripts generate-dashboard-login` and following the steps. +Restart `migrations` and it should insert the new user into the database. \ No newline at end of file diff --git a/config/authorization/config.ex b/config/authorization/config.ex index 1983140..fcb6d7e 100644 --- a/config/authorization/config.ex +++ b/config/authorization/config.ex @@ -107,6 +107,19 @@ defmodule Acl.UserGroups.Config do }" end + defp can_access_dashboard() do + %AccessByQuery{ + vars: [], + query: "PREFIX ext: + PREFIX mu: + SELECT DISTINCT ?account WHERE { + ?account. + ?account ?session_role. + FILTER( ?session_role = \"dashboard-user\" ) + }" + } + end + def user_groups do [ %GroupSpec{ @@ -206,6 +219,46 @@ defmodule Acl.UserGroups.Config do } ] }, + # // dashboard users + %GroupSpec{ + name: "dashboard-users", + useage: [:read], + access: can_access_dashboard(), + graphs: [ + %GraphSpec{ + graph: "http://mu.semte.ch/graphs/reports", + constraint: %ResourceConstraint{ + resource_types: [ + "http://lblod.data.gift/vocabularies/reporting/Report", + "http://open-services.net/ns/core#Error", + "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#DataContainer", + "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject" + ] + } + }, + %GraphSpec{ + graph: "http://mu.semte.ch/graphs/system/jobs", + constraint: %ResourceConstraint{ + resource_types: [ + "http://vocab.deri.ie/cogs#Job", + "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#DataContainer", + "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject" + ] + } + }, + # OP stores jobs in multiple graphs, so we configure both until they are merged into one graph + %GraphSpec{ + graph: "http://mu.semte.ch/graphs/jobs", + constraint: %ResourceConstraint{ + resource_types: [ + "http://vocab.deri.ie/cogs#Job", + "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#DataContainer", + "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject" + ] + } + }, + ] + }, # // CLEANUP # diff --git a/config/dispatcher/dispatcher.ex b/config/dispatcher/dispatcher.ex index a58f69a..f647abf 100644 --- a/config/dispatcher/dispatcher.ex +++ b/config/dispatcher/dispatcher.ex @@ -240,6 +240,27 @@ defmodule Dispatcher do Proxy.forward(conn, path, "http://resource/kbo-organizations/") end + # Dashboard + match "/jobs/*path", %{accept: [:json], layer: :api} do + Proxy.forward(conn, path, "http://cache/jobs/") + end + + match "/tasks/*path", %{accept: [:json], layer: :api} do + Proxy.forward(conn, path, "http://cache/tasks/") + end + + match "/data-containers/*path", %{accept: [:json], layer: :api} do + Proxy.forward(conn, path, "http://cache/data-containers/") + end + + match "/job-errors/*path", %{accept: [:json], layer: :api} do + Proxy.forward(conn, path, "http://cache/job-errors/") + end + + match "/reports/*path", %{accept: [:json], layer: :api} do + Proxy.forward(conn, path, "http://resource/reports/") + end + ############### # LOGIN ############### @@ -264,6 +285,10 @@ defmodule Dispatcher do Proxy.forward(conn, path, "http://mocklogin/sessions/") end + match "/sessions/*path", %{ reverse_host: ["dashboard" | _rest] } do + Proxy.forward conn, path, "http://dashboard-login/sessions/" + end + match "/sessions/*path", %{accept: [:any], layer: :api} do Proxy.forward(conn, path, "http://login/sessions/") end @@ -428,6 +453,20 @@ defmodule Dispatcher do # frontend layer ############################################################### + get "/assets/*path", %{ reverse_host: ["dashboard" | _rest], layer: :api } do + forward conn, path, "http://frontend-dashboard/assets/" + end + + get "/@appuniversum/*path", %{ reverse_host: ["dashboard" | _rest], layer: :api} do + forward conn, path, "http://frontend-dashboard/@appuniversum/" + end + + match "/*_path", %{ reverse_host: ["dashboard" | _rest], layer: :api } do + # *_path allows a path to be supplied, but will not yield + # an error that we don't use the path variable. + forward conn, [], "http://frontend-dashboard/index.html" + end + match "/assets/*path", %{layer: :api} do Proxy.forward(conn, path, "http://frontend/assets/") end diff --git a/config/resources/domain.lisp b/config/resources/domain.lisp index a9fa35f..18fb9c0 100644 --- a/config/resources/domain.lisp +++ b/config/resources/domain.lisp @@ -20,3 +20,5 @@ (read-domain-file "files-domain.lisp") (read-domain-file "dcat.json") (read-domain-file "privacy-centric-service.json") +(read-domain-file "job.json") +(read-domain-file "report.json") diff --git a/config/resources/files-domain.lisp b/config/resources/files-domain.lisp index b9dfdcf..36589ef 100644 --- a/config/resources/files-domain.lisp +++ b/config/resources/files-domain.lisp @@ -7,7 +7,10 @@ (:created :datetime ,(s-prefix "nfo:fileCreated"))) :has-one `((file :via ,(s-prefix "nie:dataSource") :inverse t - :as "download")) + :as "download") + (data-container :via ,(s-prefix "task:hasFile") + :inverse t + :as "data-container")) :resource-base (s-url "http://data.lblod.info/files/") :features `(no-pagination-defaults include-uri) :on-path "files") diff --git a/config/resources/job.json b/config/resources/job.json new file mode 100644 index 0000000..dba13a1 --- /dev/null +++ b/config/resources/job.json @@ -0,0 +1,147 @@ +{ + "version": "0.1", + "prefixes": { + "adms": "http://www.w3.org/ns/adms#", + "cogs": "http://vocab.deri.ie/cogs#", + "dct": "http://purl.org/dc/terms/", + "oslc": "http://open-services.net/ns/core#", + "nfo": "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#", + "task": "http://redpencil.data.gift/vocabularies/tasks/" + }, + "resources": { + "jobs": { + "name": "job", + "class": "cogs:Job", + "attributes": { + "created": { + "type": "datetime", + "predicate": "dct:created" + }, + "modified": { + "type": "datetime", + "predicate": "dct:modified" + }, + "creator": { + "type": "url", + "predicate": "dct:creator" + }, + "status": { + "type": "url", + "predicate": "adms:status" + }, + "operation": { + "type": "url", + "predicate": "task:operation" + } + }, + "relationships": { + "error": { + "predicate": "task:error", + "target": "job-error", + "cardinality": "one" + }, + "tasks": { + "predicate": "dct:isPartOf", + "target": "group", + "cardinality": "many", + "inverse": true + } + }, + "features": ["include-uri"], + "new-resource-base": "http://redpencil.data.gift/id/job/" + }, + "tasks": { + "name": "task", + "class": "task:Task", + "attributes": { + "created": { + "type": "datetime", + "predicate": "dct:created" + }, + "modified": { + "type": "datetime", + "predicate": "dct:modified" + }, + "status": { + "type": "url", + "predicate": "adms:status" + }, + "operation": { + "type": "url", + "predicate": "task:operation" + }, + "index": { + "type": "string", + "predicate": "task:index" + } + }, + "relationships": { + "error": { + "predicate": "task:error", + "target": "job-error", + "cardinality": "one" + }, + "job": { + "predicate": "dct:isPartOf", + "target": "job", + "cardinality": "one" + }, + "parent-tasks": { + "predicate": "cogs:dependsOn", + "target": "task", + "cardinality": "many" + }, + "results-containers": { + "predicate": "task:resultsContainer", + "target": "data-container", + "cardinality": "many" + }, + "input-containers": { + "predicate": "task:inputContainer", + "target": "data-container", + "cardinality": "many" + } + }, + "features": ["include-uri"], + "new-resource-base": "http://redpencil.data.gift/id/task/" + }, + "job-errors": { + "name": "job-error", + "class": "oslc:Error", + "attributes": { + "message": { + "type": "string", + "predicate": "oslc:message" + } + }, + "features": ["include-uri"], + "new-resource-base": "http://redpencil.data.gift/id/jobs/error/" + }, + "data-containers": { + "name": "data-container", + "class": "nfo:DataContainer", + "attributes": { + "has-graph": { + "type": "url", + "predicate": "task:hasGraph" + } + }, + "relationships": { + "result-from-tasks": { + "predicate": "task:resultsContainer", + "target": "task", + "cardinality": "many", + "inverse": true + }, + "input-from-tasks": { + "predicate": "task:inputContainer", + "target": "task", + "cardinality": "many", + "inverse":true + } + }, + "features": ["include-uri"], + "new-resource-base": "http://redpencil.data.gift/id/dataContainers/" + } + } +} \ No newline at end of file diff --git a/config/resources/report.json b/config/resources/report.json new file mode 100644 index 0000000..2463efe --- /dev/null +++ b/config/resources/report.json @@ -0,0 +1,36 @@ +{ + "version": "0.1", + "prefixes": { + "dct": "http://purl.org/dc/terms/", + "prov": "http://www.w3.org/ns/prov#" + }, + "resources": { + "reports": { + "name": "report", + "class": "http://lblod.data.gift/vocabularies/reporting/Report", + "attributes": { + "created": { + "type": "datetime", + "predicate": "dct:created" + }, + "title": { + "type": "string", + "predicate": "dct:title" + }, + "description": { + "type": "string", + "predicate": "dct:description" + } + }, + "relationships": { + "file": { + "predicate": "prov:generated", + "target": "file", + "cardinality": "one" + } + }, + "features": ["include-uri"], + "new-resource-base": "http://data.lblod.info/id/reports/" + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6a8aa37..162eaf3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -147,6 +147,14 @@ services: logging: *default-logging links: - db:database + dashboard-login: + image: semtech/mu-login-service:3.0.0 + links: + - db:database + labels: + - "logging=true" + restart: always + logging: *default-logging accountdetail: image: lblod/account-detail-service:0.1.0 labels: @@ -202,6 +210,12 @@ services: - "logging=true" restart: always logging: *default-logging + frontend-dashboard: + image: lblod/frontend-dashboard:1.7.0 + labels: + - "logging=true" + restart: always + logging: *default-logging uri-info: image: redpencil/mu-uri-info-service:0.2.1 links: diff --git a/scripts/project/config.json b/scripts/project/config.json index 2a8ae6f..471f975 100644 --- a/scripts/project/config.json +++ b/scripts/project/config.json @@ -16,6 +16,21 @@ "mounts": { "app": "/data/app/" } + }, + { + "documentation": { + "command": "generate-dashboard-login", + "description": "generate a login account for the jobs dashboard", + "arguments": [] + }, + "environment": { + "image": "ruby", + "interactive": true, + "script": "generate-dashboard-login/run.sh" + }, + "mounts": { + "app": "/project/" + } } ] -} +} \ No newline at end of file diff --git a/scripts/project/generate-dashboard-login/run.sh b/scripts/project/generate-dashboard-login/run.sh new file mode 100755 index 0000000..03f4aba --- /dev/null +++ b/scripts/project/generate-dashboard-login/run.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env ruby +require 'bundler/inline' +require 'yaml' +require 'securerandom' +require 'date' + +$stdout.sync = true +print "installing dependencies..." +gemfile do + source 'https://rubygems.org' + gem 'bcrypt' +end +puts "done" + +salt = "" +begin + File.open("/project/docker-compose.override.yml") do |file| + config = YAML.load(file) + salt = config.dig("services", "dashboard-login", "environment", "MU_APPLICATION_SALT") + throw "invalid salt" unless salt + end +rescue + puts "Tried to locate login salt in docker-compose.override.yml, but couldn't find it." + puts "Please make sure a salt is configured:" + puts < +PREFIX people: +PREFIX accounts: +PREFIX dcterms: +PREFIX mu: +PREFIX ext: +PREFIX account: +PREFIX xsd: +INSERT DATA { + GRAPH { + people:#{person_uuid} a foaf:Person ; + foaf:name "#{account_name}"; + foaf:account accounts:#{account_uuid}; + mu:uuid "#{person_uuid}"; + dcterms:created "#{now}"^^xsd:datetime; + dcterms:modified "#{now}"^^xsd:datetime. + accounts:#{account_uuid} a foaf:OnlineAccount; + foaf:accountName "#{account_name}"; + mu:uuid "#{account_uuid}"; + account:password """#{hashed_password}"""; + account:salt "#{account_salt}"; + account:status ; + dcterms:created "#{now}"^^xsd:datetime; + ext:sessionRole "dashboard-user"; + dcterms:modified "#{now}"^^xsd:datetime. +}} +) + +timestamp = DateTime.now.strftime("%Y%m%d%H%M%L") +filename = "#{timestamp}-create-user-#{account_name.gsub(" ", "-")}.sparql" +File.write("/project/config/migrations/local/#{filename}", query) +puts "generated migration ./config/migrations/local/#{filename} for #{account_name.inspect} : #{password.inspect}" \ No newline at end of file