view pkg/models/intservices.go @ 5601:1222b777f51f

Made golint finally happy.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Sat, 06 Aug 2022 02:09:57 +0200
parents 5f47eeea988d
children
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
//  * Tom Gottfried <tom.gottfried@intevation.de>

package models

import (
	"context"
	"database/sql"
	"net/http"
	"sync"

	"gemma.intevation.de/gemma/pkg/auth"
	"gemma.intevation.de/gemma/pkg/config"
	"gemma.intevation.de/gemma/pkg/log"
)

// DatabaseScheme is the schema used for the data
// accessible for the waterway user.
const DatabaseScheme = "waterway"

type (
	// IntEntry repreents a published layer in the GeoServer.
	IntEntry struct {
		Schema           string  `json:"schema"`
		Name             string  `json:"name"`
		SQL              *string `json:"sql"`
		KeyColumn        *string `json:"keycolumn"`
		SRS              *string `json:"srs"`
		Style            bool    `json:"style"`
		WMS              bool    `json:"wms"`
		WFS              bool    `json:"wfs"`
		WMSTAttribute    *string `json:"wmst-attribute"`
		WMSTEndAttribute *string `json:"wmst-end-attribute"`
	}

	// LayerGroup is a layer group in a published GeoServer layer.
	LayerGroup struct {
		Name   string   `json:"name"`
		Layers []string `json:"layers"`
	}

	// IntServices are the internal services published by
	// the GeoServer.
	IntServices struct {
		mu          sync.Mutex
		entries     []IntEntry
		layerGroups []LayerGroup
	}
)

const (
	selectServicesSQL = `
SELECT
  schema,
  name,
  view_def,
  key_column,
  auth_name || ':' || auth_srid,
  style IS NOT NULL,
  as_wms,
  as_wfs,
  wmst_attribute,
  wmst_end_attribute
FROM sys_admin.published_services
  LEFT JOIN spatial_ref_sys USING (srid)
WHERE schema = $1
ORDER by name`

	selectGroupedLayersSQL = `
SELECT group_name, name
FROM sys_admin.grouped_layers
ORDER BY group_name, ord`

	selectStyleSQL = `
SELECT style
FROM sys_admin.published_services
WHERE name = $1 AND schema = $2`

	updateStyleSQL = `
UPDATE sys_admin.published_services
SET style = $1
WHERE name = $2 AND schema = $3`
)

// InternalServices is the list of the internal services
// managed by the Gemma server.
var InternalServices = &IntServices{}

// LoadStyle a style for a given entry.
func (e *IntEntry) LoadStyle() ([]byte, error) {
	var style []byte
	ctx := context.Background()
	err := auth.RunAs(ctx, "sys_admin",
		func(conn *sql.Conn) error {
			return conn.QueryRowContext(
				ctx,
				selectStyleSQL,
				e.Name, e.Schema).Scan(&style)
		})
	return style, err
}

// UpdateInternalStyle updates a style in the database.
func UpdateInternalStyle(req *http.Request, name string, style []byte) error {
	return auth.RunAsSessionUser(req, func(conn *sql.Conn) error {
		_, err := conn.ExecContext(
			req.Context(), updateStyleSQL,
			style, name, DatabaseScheme)
		if err == nil {
			InternalServices.Invalidate()
		}
		return err
	})
}

// LayerGroups returns the list of layer groups from the database.
func (ps *IntServices) LayerGroups() []LayerGroup {
	ps.mu.Lock()
	defer ps.mu.Unlock()

	if ps.entries == nil {
		if err := ps.load(); err != nil {
			log.Errorf("%v\n", err)
			return nil
		}
	}

	// To avoid races we simple make a deep copy.
	// As we don't have such many of them it light weight enough for now.
	groups := make([]LayerGroup, len(ps.layerGroups))
	for i := range groups {
		layers := make([]string, len(ps.layerGroups[i].Layers))
		copy(layers, ps.layerGroups[i].Layers)
		groups[i] = LayerGroup{Name: ps.layerGroups[i].Name, Layers: layers}
	}
	return groups
}

// Find looks for an internal service with a given name.
func (ps *IntServices) Find(name string) (string, bool) {
	ps.mu.Lock()
	defer ps.mu.Unlock()

	if ps.entries == nil {
		if err := ps.load(); err != nil {
			log.Errorf("%v\n", err)
			return "", false
		}
	}

	if ps.has(name) {
		return config.GeoServerURL() + "/" + name, true
	}
	return "", false
}

func (ps *IntServices) has(service string) bool {
	var check func(*IntEntry) bool
	switch service {
	case "wms":
		check = func(e *IntEntry) bool { return e.WMS }
	case "wfs":
		check = func(e *IntEntry) bool { return e.WFS }
	default:
		return false
	}
	for i := range ps.entries {
		if check(&ps.entries[i]) {
			return true
		}
	}
	return false
}

func (ps *IntServices) load() error {
	// make empty slice to prevent retry if slice is empty.
	ps.entries = []IntEntry{}
	ps.layerGroups = []LayerGroup{}
	ctx := context.Background()

	// Load the internal layers.
	entries := func(conn *sql.Conn) error {
		rows, err := conn.QueryContext(
			ctx, selectServicesSQL, DatabaseScheme)
		if err != nil {
			return err
		}
		defer rows.Close()
		for rows.Next() {
			var entry IntEntry
			if err := rows.Scan(
				&entry.Schema, &entry.Name,
				&entry.SQL, &entry.KeyColumn, &entry.SRS, &entry.Style,
				&entry.WMS, &entry.WFS,
				&entry.WMSTAttribute, &entry.WMSTEndAttribute,
			); err != nil {
				return err
			}
			ps.entries = append(ps.entries, entry)
		}
		return rows.Err()
	}

	// Load the layer groups.
	groups := func(conn *sql.Conn) error {
		rows, err := conn.QueryContext(ctx, selectGroupedLayersSQL)
		if err != nil {
			return err
		}
		defer rows.Close()

		for rows.Next() {
			var group, layer string
			if err := rows.Scan(&group, &layer); err != nil {
				return err
			}
			if n := len(ps.layerGroups); n > 0 && ps.layerGroups[n-1].Name == group {
				ps.layerGroups[n-1].Layers = append(ps.layerGroups[n-1].Layers, layer)
			} else {
				ps.layerGroups = append(ps.layerGroups, LayerGroup{
					Name:   group,
					Layers: []string{layer},
				})
			}
		}
		return rows.Err()
	}

	return auth.RunAllAs(ctx, "sys_admin", entries, groups)
}

// Invalidate invalidates the list on internal services.
func (ps *IntServices) Invalidate() {
	ps.mu.Lock()
	ps.entries = nil
	ps.layerGroups = nil
	ps.mu.Unlock()
}

// InternalAll passes all entries.
func InternalAll(IntEntry) bool { return true }

// IntWMS passes only WMS entries.
func IntWMS(entry IntEntry) bool { return entry.WMS }

// IntWFS passes only WFS entries.
func IntWFS(entry IntEntry) bool { return entry.WFS }

// IntSQLView passes only entries with a SQL view.
func IntSQLView(entry IntEntry) bool { return entry.SQL != nil }

// IntWithStyle passes only entries with a style.
func IntWithStyle(entry IntEntry) bool { return entry.Style }

// IntByName passes only entries with a given name.
func IntByName(name string) func(IntEntry) bool {
	return func(entry IntEntry) bool { return entry.Name == name }
}

// IntAnd filters internal entries that match all conditions.
func IntAnd(accept ...func(IntEntry) bool) func(IntEntry) bool {
	return func(entry IntEntry) bool {
		for _, a := range accept {
			if !a(entry) {
				return false
			}
		}
		return true
	}
}

// Filter returns a list of internal services that match the
// given condition.
func (ps *IntServices) Filter(accept func(IntEntry) bool) []IntEntry {
	ps.mu.Lock()
	defer ps.mu.Unlock()
	if ps.entries == nil {
		if err := ps.load(); err != nil {
			log.Errorf("%v\n", err)
			return nil
		}
	}
	pe := make([]IntEntry, 0, len(ps.entries))
	for _, e := range ps.entries {
		if accept(e) {
			pe = append(pe, e)
		}
	}

	return pe
}