Mercurial > gemma
changeset 1365:da7ee82ad5d6
merge
author | Fadi Abbud <fadi.abbud@intevation.de> |
---|---|
date | Tue, 27 Nov 2018 09:03:27 +0100 |
parents | 0c5cbbafbd94 (current diff) 3ff916e853d4 (diff) |
children | a25a4d4a3e6e 7550a4707863 |
files | client/src/components/map/contextbox/Bottlenecks.vue client/src/components/map/contextbox/Staging.vue client/src/store/imports.js client/src/store/index.js |
diffstat | 22 files changed, 277 insertions(+), 211 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/map/contextbox/ImportSoundingresults.vue Mon Nov 26 12:44:52 2018 +0100 +++ b/client/src/components/map/contextbox/ImportSoundingresults.vue Tue Nov 27 09:03:27 2018 +0100 @@ -1,87 +1,100 @@ <template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon>Import Soundingresults - </h6> - <div v-if="editState" class="ml-auto mr-auto mt-4 w-95"> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label> - </div> - <input - id="bottleneck" - type="text" - class="form-control" - placeholder="Name of Bottleneck" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="bottleneck" - > - </div> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label class="label-text" for="importdate" id="importdatelabel">Date</label> - </div> - <input - id="importdate" - type="date" - class="form-control" - placeholder="Date of import" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="importDate" - > - <div class="offset-r"> - <label - class="label-text w-100 depthreferencelabel" - for="depthreference" - >Depthreference</label> - </div> - <select v-model="depthReference" class="custom-select" id="depthreference"> - <option - v-for="option in this.$options.depthReferenceOptions" - :key="option" - >{{option}}</option> - </select> - </div> - <div class="text-left"> - <small v-for="(message, index) in messages" :key="index">{{message}}</small> - </div> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon>Import + Soundingresults + </h6> + <div v-if="editState" class="ml-auto mr-auto mt-4 w-95"> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label> + </div> + <div class="d-flex flex-column"> + <select v-model="bottleneck" class="custom-select"> + <option v-for="bottleneck in availableBottlenecks" :key="bottleneck">{{bottleneck}}</option> + </select> + <span class="text-left text-danger"> + <small v-if="!bottleneck">Please select a bottleneck</small> + </span> + </div> + </div> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label class="label-text" for="importdate" id="importdatelabel">Date</label> + </div> + <div class="d-flex flex-column"> + <input + id="importdate" + type="date" + class="form-control" + placeholder="Date of import" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="importDate" + > + <span class="text-left text-danger"> + <small v-if="!importDate">Please enter a date</small> + </span> + </div> + <div class="offset-r"> + <label class="label-text w-100 depthreferencelabel" for="depthreference">Depthreference</label> + </div> + <div class="d-flex flex-column"> + <select v-model="depthReference" class="custom-select" id="depthreference"> + <option v-for="option in this.$options.depthReferenceOptions" :key="option">{{ option }}</option> + </select> + <span class="text-left text-danger"> + <small v-if="!depthReference">Please enter a reference</small> + </span> </div> - <div class="w-95 ml-auto mr-auto mt-4 mb-4"> - <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> - <div class="custom-file"> - <input - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - > - <label class="custom-file-label" for="uploadFile">{{uploadLabel}}</label> - </div> - </div> - <div class="buttons text-right"> - <a - v-if="editState" - download="meta.json" - :href="dataLink " - class="btn btn-outline-info pull-left" - >Download Meta.json</a> - <button - v-if="editState" - @click="deleteTempData" - class="btn btn-danger" - type="button" - >Cancel Upload</button> - <button - :disabled="disableUpload" - @click="submit" - class="btn btn-info" - type="button" - >{{uploadState?"Upload":"Confirm"}}</button> - </div> + </div> + <div class="text-left"> + <small v-for="(message, index) in messages" :key="index"> + {{ + message + }} + </small> + </div> + </div> + <div class="w-95 ml-auto mr-auto mt-4 mb-4"> + <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> + <div class="custom-file"> + <input + accept=".zip" + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + > + <label class="custom-file-label" for="uploadFile"> + {{ + uploadLabel + }} + </label> </div> + </div> + <div class="buttons text-right"> + <a + v-if="editState" + download="meta.json" + :href="dataLink" + class="btn btn-outline-info pull-left" + >Download Meta.json</a> + <button + v-if="editState" + @click="deleteTempData" + class="btn btn-danger" + type="button" + >Cancel Upload</button> + <button + :disabled="disableUploadButton" + @click="submit" + class="btn btn-info" + type="button" + >{{ uploadState ? "Upload" : "Confirm" }}</button> + </div> </div> + </div> </template> <script> @@ -91,7 +104,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later * License-Filename: LICENSES/AGPL-3.0.txt * - * Copyright (C) 2018 by via donau + * Copyright (C) 2018 by via donau * – Österreichische Wasserstraßen-Gesellschaft mbH * Software engineering by Intevation GmbH * @@ -101,6 +114,7 @@ */ import { HTTP } from "../../../lib/http"; import { displayError, displayInfo } from "../../../lib/errors.js"; +import { mapState } from "vuex"; const defaultLabel = "Choose .zip-file"; const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; @@ -173,12 +187,14 @@ } }) .then(response => { - const { bottleneck, date } = response.data.meta; - const depthReference = response.data.meta["depth-reference"]; + if (response.data.meta) { + const { bottleneck, date } = response.data.meta; + const depthReference = response.data.meta["depth-reference"]; + this.bottleneck = bottleneck; + this.depthReference = depthReference; + this.importDate = new Date(date).toISOString().split("T")[0]; + } this.importState = IMPORTSTATE.EDIT; - this.bottleneck = bottleneck; - this.depthReference = depthReference; - this.importDate = new Date(date).toISOString().split("T")[0]; this.token = response.data.token; this.messages = response.data.messages; }) @@ -222,7 +238,26 @@ }); } }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + }, + watch: { + showContextBox() { + if (!this.showContextBox && this.token) this.deleteTempData(); + } + }, computed: { + ...mapState("application", ["showContextBox"]), + ...mapState("bottlenecks", ["bottlenecks"]), + disableUploadButton() { + if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; + if (!this.bottleneck || !this.importDate || !this.depthReference) + return true; + return this.disableUpload; + }, + availableBottlenecks() { + return this.bottlenecks.map(x => x.properties.name); + }, editState() { return this.importState === IMPORTSTATE.EDIT; },
--- a/client/src/components/map/contextbox/Staging.vue Mon Nov 26 12:44:52 2018 +0100 +++ b/client/src/components/map/contextbox/Staging.vue Tue Nov 27 09:03:27 2018 +0100 @@ -6,10 +6,10 @@ <table class="table mb-0"> <thead> <tr> - <th>Name</th> + <th>id</th> <th>Datatype</th> <th>Importdate</th> - <th>ImportID</th> + <th>Username</th> <th> </th> <th> </th> </tr> @@ -17,11 +17,11 @@ <tbody v-if="filteredData.length"> <tr v-for="data in filteredData" :key="data.id"> <td> - <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a> + <a @click="zoomTo(data.location)" href="#">{{ data.id }}</a> </td> - <td>{{ data.type }}</td> - <td>{{ data.date }}</td> - <td>{{ data.importID }}</td> + <td>{{ data.kind }}</td> + <td>{{ data.enqueued }}</td> + <td>{{ data.user }}</td> <td> <button @click="toggleApproval(data.id, $options.STATES.APPROVED)" @@ -68,56 +68,12 @@ * Markus Kottländer <markus@intevation.de> */ import { mapState } from "vuex"; - +import { STATES } from "../../../store/imports.js"; import { displayError, displayInfo } from "../../../lib/errors.js"; export default { - STATES: { - NEEDSAPPROVAL: "NEEDSAPPROVAL", - APPROVED: "APPROVED", - REJECTED: "REJECTED" - }, data() { - return { - demodata: [ - { - id: 1, - name: "B1", - date: "2018-11-19 10:23", - location: [16.5364, 48.1471], - status: "NEEDSAPPROVAL", - importID: "123456789", - type: "bottleneck" - }, - { - id: 2, - name: "B2", - date: "2018-11-19 10:24", - location: [16.5364, 48.1472], - status: "NEEDSAPPROVAL", - importID: "123456789", - type: "bottleneck" - }, - { - id: 3, - name: "s1", - date: "2018-11-13 10:25", - location: [16.5364, 48.1473], - status: "NEEDSAPPROVAL", - importID: "987654321", - type: "soundingresult" - }, - { - id: 4, - name: "s2", - date: "2018-11-13 10:26", - location: [16.5364, 48.1474], - status: "NEEDSAPPROVAL", - importID: "987654321", - type: "soundingresult" - } - ] - }; + return {}; }, mounted() { this.$store.dispatch("imports/getStaging").catch(error => { @@ -130,46 +86,22 @@ }, computed: { ...mapState("application", ["searchQuery"]), + ...mapState("imports", ["staging"]), filteredData() { - return this.demodata.filter(data => { - const nameFound = data.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const dateFound = data.date - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const locationFound = data.location.find(coord => { - return coord - .toString() - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }); - const statusFound = data.status - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const importIDFound = data.importID - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const typeFound = data.type - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - - return ( - nameFound || - dateFound || - locationFound || - statusFound || - importIDFound || - typeFound + return this.staging.filter(data => { + const result = [data.id + "", data.enqueued, data.kind, data.user].some( + x => x.toLowerCase().includes(this.searchQuery.toLowerCase()) ); + return result; }); } }, + STATES: STATES, methods: { confirmReview() { - const message = this.demodata + const message = this.staging .map(x => { - return x.name + ": " + x.status; + return x.id + ": " + x.status; }) .join("\n"); displayInfo({ @@ -178,15 +110,16 @@ }); }, needsApproval(item) { - return item.status === this.$options.STATES.NEEDSAPPROVAL; + return item.status === STATES.NEEDSAPPROVAL; }, isRejected(item) { - return item.status === this.$options.STATES.REJECTED; + return item.status === STATES.REJECTED; }, isApproved(item) { - return item.status === this.$options.STATES.APPROVED; + return item.status === STATES.APPROVED; }, zoomTo(coordinates) { + if (!coordinates) return; this.$store.commit("map/moveMap", { coordinates: coordinates, zoom: 17, @@ -194,14 +127,10 @@ }); }, toggleApproval(id, newStatus) { - const stagedResult = this.demodata.find(e => { - return e.id === id; + this.$store.commit("imports/toggleApproval", { + id: id, + newStatus: newStatus }); - if (stagedResult.status === newStatus) { - stagedResult.status = this.$options.STATES.NEEDSAPPROVAL; - } else { - stagedResult.status = newStatus; - } } } };
--- a/client/src/store/imports.js Mon Nov 26 12:44:52 2018 +0100 +++ b/client/src/store/imports.js Tue Nov 27 09:03:27 2018 +0100 @@ -30,7 +30,7 @@ }; }; -export default { +const imports = { init, namespaced: true, state: init(), @@ -39,19 +39,21 @@ state.imports = imports; }, setStaging: (state, staging) => { - state.staging = staging; + const enriched = staging.map(x => { + return { ...x, status: STATES.NEEDSAPPROVAL }; + }); + state.staging = enriched; }, toggleApproval: (state, change) => { - throw "Not implemented!"; - const { id, newState } = change; - const stagedResult = this.state.staging.find(e => { + const { id, newStatus } = change; + const stagedResult = state.staging.find(e => { return e.id === id; }); - if (stagedResult.status === newState) { - stagedResult.status = this.$options.STATES.NEEDSAPPROVAL; + if (stagedResult.status === newStatus) { + stagedResult.status = STATES.NEEDSAPPROVAL; } else { - stagedResult.status = newState; + stagedResult.status = newStatus; } } }, @@ -101,3 +103,5 @@ } } }; + +export { imports, STATES };
--- a/client/src/store/index.js Mon Nov 26 12:44:52 2018 +0100 +++ b/client/src/store/index.js Tue Nov 27 09:03:27 2018 +0100 @@ -21,7 +21,7 @@ import map from "./map"; import fairwayprofile from "./fairway"; import bottlenecks from "./bottlenecks"; -import imports from "./imports"; +import { imports } from "./imports"; Vue.use(Vuex);
--- a/docker/Dockerfile.backend Mon Nov 26 12:44:52 2018 +0100 +++ b/docker/Dockerfile.backend Tue Nov 27 09:03:27 2018 +0100 @@ -1,5 +1,7 @@ FROM ubuntu:bionic LABEL authors="tom.gottfried@intevation.de" +LABEL description="Contains software from gemma, for right holders and\ + licensing infos, see https://hg.intevation.de/gemma ." RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
--- a/docker/Dockerfile.db Mon Nov 26 12:44:52 2018 +0100 +++ b/docker/Dockerfile.db Tue Nov 27 09:03:27 2018 +0100 @@ -1,5 +1,7 @@ FROM ubuntu:bionic LABEL authors="tom.gottfried@intevation.de" +LABEL description="Contains software from gemma, for right holders and\ + licensing infos, see https://hg.intevation.de/gemma ." RUN apt-get update &&\ apt-get -y install --no-install-recommends curl gnupg
--- a/docker/Dockerfile.geoserv Mon Nov 26 12:44:52 2018 +0100 +++ b/docker/Dockerfile.geoserv Tue Nov 27 09:03:27 2018 +0100 @@ -1,5 +1,7 @@ FROM ubuntu:bionic LABEL authors="tom@intevation.de" +LABEL description="Contains software from gemma, for right holders and\ + licensing infos, see https://hg.intevation.de/gemma ." RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
--- a/docker/Dockerfile.spa Mon Nov 26 12:44:52 2018 +0100 +++ b/docker/Dockerfile.spa Tue Nov 27 09:03:27 2018 +0100 @@ -1,5 +1,7 @@ FROM ubuntu:bionic LABEL authors="tom@intevation.de" +LABEL description="Contains software from gemma, for right holders and\ + licensing infos, see https://hg.intevation.de/gemma ." RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
--- a/docker/README.md Mon Nov 26 12:44:52 2018 +0100 +++ b/docker/README.md Tue Nov 27 09:03:27 2018 +0100 @@ -34,7 +34,7 @@ ## Create ER diagrams -Assuming you have installed postgresql_autodoc and graphviz on a machine +Assuming you have installed `postgresql_autodoc` and `graphviz` on a machine from wich you can reach your docker host, you can use the following: - ER diagram with waterway related tables:
--- a/pkg/auth/middleware.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/auth/middleware.go Tue Nov 27 09:03:27 2018 +0100 @@ -26,16 +26,26 @@ tokenKey ) +// GetSession returns the session stored in the context of the request. func GetSession(req *http.Request) (*Session, bool) { session, ok := req.Context().Value(sessionKey).(*Session) return session, ok } +// GetToken returns the session token associated with given request. func GetToken(req *http.Request) (string, bool) { token, ok := req.Context().Value(tokenKey).(string) return token, ok } +// SessionMiddleware constructs a middleware to enforce the existence +// of the header X-Gemma-Auth in the incoming request and checks +// if a session is bound to it. +// Ihe the checks fail the constructed handler issues an http.StatusUnauthorized +// back to the invokation stacks and prevents the execution of the +// nested http.Handler next. +// Inside the http.Handler next calls to GetSession and GetToken are valid +// to fetch the respective information. func SessionMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { @@ -63,6 +73,10 @@ }) } +// SessionChecker constructs a middleware to check invariants about a session +// before calling the nested http.Handler next. +// This is useful when creating specialized middleware e.g. to enforce +// a role system. func SessionChecker(next http.Handler, check func(*Session) bool) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if claims, ok := GetSession(req); !ok || !check(claims) { @@ -73,12 +87,15 @@ }) } +// HasRole is a checker function fitting into SessionChecker to check +// if the user is logged in with at least one of list of given roles. func HasRole(roles ...string) func(*Session) bool { return func(session *Session) bool { return session.Roles.HasAny(roles...) } } +// EnsureRole is a macro function to stitch SessionChecker and HasRole together. func EnsureRole(roles ...string) func(http.Handler) http.Handler { return func(handler http.Handler) http.Handler { return SessionMiddleware(SessionChecker(handler, HasRole(roles...)))
--- a/pkg/auth/opendb.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/auth/opendb.go Tue Nov 27 09:03:27 2018 +0100 @@ -27,10 +27,14 @@ ) var ( + // ErrNoMetamorphUser is returned if no metamorphic user is configured. ErrNoMetamorphUser = errors.New("No metamorphic user configured") - ErrNotLoggedIn = errors.New("Not logged in") + // ErrNotLoggedIn is returned if there is the user is not logged in. + ErrNotLoggedIn = errors.New("Not logged in") ) +// OpenDB opens up a database connection with a given username and password. +// The other credentials are taken from the configuration. func OpenDB(user, password string) (*sql.DB, error) { // To ease SSL config ride a bit on parsing. @@ -74,7 +78,7 @@ return db, nil } -func MetamorphConn(ctx context.Context, user string) (*sql.Conn, error) { +func metamorphConn(ctx context.Context, user string) (*sql.Conn, error) { db, err := mm.open() if err != nil { return nil, err @@ -102,6 +106,8 @@ WHERE oid IN (SELECT oid FROM cte) AND rolname <> current_user AND EXISTS (SELECT 1 FROM users.list_users WHERE username = current_user)` +// AllOtherRoles loggs in as user with password and returns a list +// of all roles the logged in user has in the system. func AllOtherRoles(user, password string) (Roles, error) { db, err := OpenDB(user, password) if err != nil { @@ -126,8 +132,12 @@ return roles, rows.Err() } +// RunAs runs a given function fn with a database connection impersonated +// as the given role. +// To make this work a metamorphic user has to be configured in +// the system configuration. func RunAs(ctx context.Context, role string, fn func(*sql.Conn) error) error { - conn, err := MetamorphConn(ctx, role) + conn, err := metamorphConn(ctx, role) if err != nil { return err } @@ -135,6 +145,8 @@ return fn(conn) } +// RunAsSessionUser is a convinience wrapper araound which extracts +// the logged in user from a session and calls RunAs with it. func RunAsSessionUser(req *http.Request, fn func(*sql.Conn) error) error { token, ok := GetToken(req) if !ok {
--- a/pkg/auth/session.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/auth/session.go Tue Nov 27 09:03:27 2018 +0100 @@ -24,18 +24,27 @@ "gemma.intevation.de/gemma/pkg/misc" ) +// Roles is a list of roles a logged in user has. type Roles []string +// Session stores the informations about a logged in user. type Session struct { - ExpiresAt int64 `json:"expires"` - User string `json:"user"` - Roles Roles `json:"roles"` + // ExpiresAt is a unix timestamp when the session + // of the user expires. + ExpiresAt int64 `json:"expires"` + + // User is the login name of the user. + User string `json:"user"` + + // Roles is the list of roles of the user. + Roles Roles `json:"roles"` // private fields for managing expiration. access time.Time mu sync.Mutex } +// Has checks if a certain role is amongst the roles. func (r Roles) Has(role string) bool { for _, x := range r { if x == role { @@ -45,6 +54,7 @@ return false } +// HasAny checks if any of the given roles is in the role list. func (r Roles) HasAny(roles ...string) bool { for _, y := range roles { if r.Has(y) { @@ -59,7 +69,8 @@ maxTokenValid = time.Hour * 3 ) -func NewSession(user, password string, roles Roles) *Session { +// newSession creates a new session. +func newSession(user, password string, roles Roles) *Session { // Create the Claims return &Session{ @@ -137,23 +148,27 @@ return access } -func GenerateSessionKey() string { +func generateSessionKey() string { return base64.URLEncoding.EncodeToString( common.GenerateRandomKey(sessionKeyLength)) } +// ErrInvalidRole is returned if a given role does not exsist in this system. var ErrInvalidRole = errors.New("Invalid role") +// GenerateSession creates a new session for a given user and password +// backed by the roles of this user in the database. func GenerateSession(user, password string) (string, *Session, error) { roles, err := AllOtherRoles(user, password) if err != nil { return "", nil, err } + // TODO: Make this a configuration. if !roles.HasAny("sys_admin", "waterway_admin", "waterway_user") { return "", nil, ErrInvalidRole } - token := GenerateSessionKey() - session := NewSession(user, password, roles) + token := generateSessionKey() + session := newSession(user, password, roles) Sessions.Add(token, session) return token, session, nil }
--- a/pkg/auth/store.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/auth/store.go Tue Nov 27 09:03:27 2018 +0100 @@ -22,11 +22,14 @@ bolt "github.com/etcd-io/bbolt" ) +// ErrNoSuchToken is returned if a given token does not +// exists th the session store. var ErrNoSuchToken = errors.New("No such token") // Sessions is the global connection pool. var Sessions *SessionStore +// SessionStore encapsulates a set of currently active sessions. type SessionStore struct { storage *bolt.DB sessions map[string]*Session @@ -35,6 +38,11 @@ var sessionsBucket = []byte("sessions") +// NewSessionStore creates a new session store. +// If the filename is empty the session are only hold in memory. +// If the filename is not empty the sessions are mirrored to +// a file with this name. Use the later option if you want +// a persistent session store. func NewSessionStore(filename string) (*SessionStore, error) { ss := &SessionStore{ @@ -125,6 +133,8 @@ } } +// Delete removes a session identified by its token from the +// session store. Returns true if there was such s session. func (ss *SessionStore) Delete(token string) bool { res := make(chan bool) ss.cmds <- func() { @@ -156,6 +166,9 @@ } } +// Add puts a session into the session store identified by +// a given token. An old session with the same key will +// be replaced. func (ss *SessionStore) Add(token string, session *Session) { res := make(chan struct{}) @@ -173,6 +186,9 @@ <-res } +// Renew refreshes a session. It takes an old token to +// identify a session and returns a new token with the +// freshed up one. func (ss *SessionStore) Renew(token string) (string, error) { type result struct { @@ -189,7 +205,7 @@ } else { delete(ss.sessions, token) ss.remove(token) - newToken := GenerateSessionKey() + newToken := generateSessionKey() // TODO: Ensure that this is not racy! session.ExpiresAt = time.Now().Add(maxTokenValid).Unix() ss.sessions[newToken] = session @@ -202,6 +218,8 @@ return r.newToken, r.err } +// Session returns the session associated with given token. +// Returns nil if no matching session was found. func (ss *SessionStore) Session(token string) *Session { res := make(chan *Session) ss.cmds <- func() { @@ -217,6 +235,7 @@ return <-res } +// Logout removes all sessions of a given user from the session store. func (ss *SessionStore) Logout(user string) { ss.cmds <- func() { for token, session := range ss.sessions { @@ -228,6 +247,8 @@ } } +// Shutdown closes the session store. +// If using the persistent mode the backing session database is closed. func (ss *SessionStore) Shutdown() error { if db := ss.storage; db != nil { log.Println("info: shutdown persistent session store.")
--- a/pkg/common/errors.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/common/errors.go Tue Nov 27 09:03:27 2018 +0100 @@ -19,6 +19,7 @@ "strings" ) +// ToError concats a slice of errors to a single error. func ToError(errs []error) error { var b strings.Builder for i, err := range errs {
--- a/pkg/common/random.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/common/random.go Tue Nov 27 09:03:27 2018 +0100 @@ -21,6 +21,8 @@ "math/big" ) +// GenerateRandomKey generates a cryptographically secure random key +// of a given length. func GenerateRandomKey(length int) []byte { k := make([]byte, length) if _, err := io.ReadFull(rand.Reader, k); err != nil { @@ -29,6 +31,9 @@ return k } +// RandomString generates a cryptographically secure password +// of a given length which consists of alpha numeric characters +// and at least one 'special' one. func RandomString(n int) string { const (
--- a/pkg/common/zip.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/common/zip.go Tue Nov 27 09:03:27 2018 +0100 @@ -18,6 +18,8 @@ "strings" ) +// FindInZIP scans a ZIP file directory for a file that ends with +// case insensitive string. Returns only the first match. func FindInZIP(z *zip.ReadCloser, needle string) *zip.File { needle = strings.ToLower(needle) for _, straw := range z.File {
--- a/pkg/controllers/importqueue.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/controllers/importqueue.go Tue Nov 27 09:03:27 2018 +0100 @@ -44,7 +44,7 @@ SELECT true FROM Waterway.imports WHERE id = $1` selectHasNoRunningImportSQL = ` -SELECT true FROM Waterway.imports +SELECT true FROM waterway.imports WHERE id = $1 AND state <> 'running'::waterway.import_state` selectImportLogsSQL = ` @@ -305,7 +305,9 @@ const ( isPendingSQL = ` -SELECT state = 'pending'::waterway.import_state, kind WHERE id = $1` +SELECT state = 'pending'::waterway.import_state, kind +FROM waterway.imports +WHERE id = $1` reviewSQL = ` UPDATE waterway.imports SET @@ -367,15 +369,17 @@ } } - if _, err = tx.ExecContext(ctx, deleteImportTrackSQL, id); err != nil { - return - } } else { if _, err = tx.ExecContext(ctx, deleteImportDataSQL, id); err != nil { return } } + // Remove the import track + if _, err = tx.ExecContext(ctx, deleteImportTrackSQL, id); err != nil { + return + } + // Log the decision and set the final state. session, _ := auth.GetSession(req) who := session.User @@ -392,5 +396,14 @@ if err = tx.Commit(); err != nil { return } + + result := struct { + Message string `json:"message"` + }{ + Message: fmt.Sprintf("Import #%d successfully changed to state '%s'.", + id, state), + } + + jr = JSONResult{Result: &result} return }
--- a/pkg/models/sr.go Mon Nov 26 12:44:52 2018 +0100 +++ b/pkg/models/sr.go Tue Nov 27 09:03:27 2018 +0100 @@ -53,6 +53,10 @@ WHERE bn.objnam = $1 AND sr.date_info = $2` ) +func (srd SoundingResultDate) MarshalJSON() ([]byte, error) { + return json.Marshal(srd.Format(SoundingResultDateFormat)) +} + func (srd *SoundingResultDate) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil {
--- a/schema/auth.sql Mon Nov 26 12:44:52 2018 +0100 +++ b/schema/auth.sql Tue Nov 27 09:03:27 2018 +0100 @@ -39,7 +39,9 @@ GRANT INSERT, UPDATE, DELETE ON users.templates, users.user_templates TO waterway_admin; GRANT INSERT, UPDATE, DELETE ON - waterway.imports, waterway.import_logs, waterway.track_imports TO waterway_admin; + waterway.imports, waterway.import_logs, waterway.track_imports, + waterway.sounding_results + TO waterway_admin; -- -- Extended privileges for sys_admin
--- a/schema/auth_tests.sql Mon Nov 26 12:44:52 2018 +0100 +++ b/schema/auth_tests.sql Tue Nov 27 09:03:27 2018 +0100 @@ -10,7 +10,6 @@ -- Author(s): -- * Tom Gottfried <tom@intevation.de> --- * Sascha Teichmann<sascha.teichmann@intevation.de> -- -- pgTAP test script for privileges and RLS policies
--- a/schema/gemma.sql Mon Nov 26 12:44:52 2018 +0100 +++ b/schema/gemma.sql Tue Nov 27 09:03:27 2018 +0100 @@ -586,11 +586,9 @@ $$ BEGIN EXECUTE format('DELETE FROM %s WHERE id = $1', OLD.relation) USING OLD.key; + RETURN NULL; END; $$ LANGUAGE plpgsql; -CREATE TRIGGER delete_import AFTER DELETE ON waterway.track_imports - FOR EACH ROW EXECUTE PROCEDURE waterway.del_import(); - COMMIT;
--- a/schema/install-db.sh Mon Nov 26 12:44:52 2018 +0100 +++ b/schema/install-db.sh Tue Nov 27 09:03:27 2018 +0100 @@ -12,6 +12,7 @@ # Author(s): # * Sascha Wilde <wilde@intevation.de> # * Tom Gottfried <tom@intevation.de> +# * Sascha L. Teichmann <sascha.teichmann@intevation.de> ME=`basename "$0"` BASEDIR=`dirname "$0"`