Add logger

This commit is contained in:
nemunaire 2023-11-24 04:18:36 +01:00
parent 6825db9699
commit e47e99d4e3
10 changed files with 342 additions and 2 deletions

View File

@ -35,6 +35,7 @@ import (
"fmt"
"log"
"net/http"
"sort"
"github.com/gin-gonic/gin"
"github.com/miekg/dns"
@ -55,6 +56,8 @@ func declareDomainsRoutes(cfg *config.Options, router *gin.RouterGroup) {
apiDomainsRoutes.PUT("", UpdateDomain)
apiDomainsRoutes.DELETE("", delDomain)
apiDomainsRoutes.GET("/logs", GetDomainLogs)
declareZonesRoutes(cfg, apiDomainsRoutes)
}
@ -144,6 +147,8 @@ func addDomain(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are unable to create your domain now."})
return
} else {
storage.MainStore.CreateDomainLog(&uz, happydns.NewDomainLog(c.MustGet("LoggedUser").(*happydns.User), happydns.LOG_INFO, fmt.Sprintf("Domain name %s added.", uz.DomainName)))
c.JSON(http.StatusOK, uz)
}
}
@ -314,3 +319,36 @@ func delDomain(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// GetDomainLogs retrieves actions recorded for the domain.
//
// @Summary Retrieve Domain actions history.
// @Schemes
// @Description Retrieve information about the actions performed on the domain by users of happyDomain.
// @Tags domains
// @Accept json
// @Produce json
// @Param domainId path string true "Domain identifier"
// @Security securitydefinitions.basic
// @Success 200 {object} []happydns.DomainLog
// @Failure 401 {object} happydns.Error "Authentication failure"
// @Failure 404 {object} happydns.Error "Domain not found"
// @Router /domains/{domainId}/logs [get]
func GetDomainLogs(c *gin.Context) {
domain := c.MustGet("domain").(*happydns.Domain)
logs, err := storage.MainStore.GetDomainLogs(domain)
if err != nil {
log.Printf("%s: An error occurs in GetDomainLogs, when retrieving logs: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to access the domain logs. Please try again later."})
return
}
// Sort by date
sort.Slice(logs, func(i, j int) bool {
return logs[i].Date.After(logs[j].Date)
})
c.JSON(http.StatusOK, logs)
}

View File

@ -314,6 +314,8 @@ func retrieveZone(c *gin.Context) {
return
}
storage.MainStore.CreateDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_INFO, fmt.Sprintf("Zone imported from provider API: %s", myZone.Id.String())))
c.JSON(http.StatusOK, &myZone.ZoneMeta)
}
@ -359,6 +361,8 @@ func importZone(c *gin.Context) {
return
}
storage.MainStore.CreateDomainLog(domain, happydns.NewDomainLog(c.MustGet("LoggedUser").(*happydns.User), happydns.LOG_INFO, fmt.Sprintf("Zone imported from Bind-style file: %s", zone.Id.String())))
c.JSON(http.StatusOK, zone)
}
@ -472,6 +476,7 @@ func applyZone(c *gin.Context) {
return
}
nbcorrections := len(form.WantedCorrections)
corrections, err := provider.GetDomainCorrections(domain, dc)
for _, cr := range corrections {
for ic, wc := range form.WantedCorrections {
@ -480,6 +485,7 @@ func applyZone(c *gin.Context) {
err := cr.F()
if err != nil {
storage.MainStore.CreateDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed zone publishing (%s): %s", zone.Id.String(), err.Error())))
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to update the zone: %s", err.Error())})
return
}
@ -492,10 +498,13 @@ func applyZone(c *gin.Context) {
}
if len(form.WantedCorrections) > 0 {
storage.MainStore.CreateDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ERR, fmt.Sprintf("Failed zone publishing (%s): %d corrections were not applied.", zone.Id.String(), nbcorrections)))
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to perform the following changes: %s", form.WantedCorrections)})
return
}
storage.MainStore.CreateDomainLog(domain, happydns.NewDomainLog(user, happydns.LOG_ACK, fmt.Sprintf("Zone published (%s), %d corrections applied with success", zone.Id.String(), nbcorrections)))
// Create a new zone in history for futher updates
newZone := zone.DerivateNew()
err = storage.MainStore.CreateZone(newZone)

76
model/logs.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright or © or Copr. happyDNS (2020)
//
// contact@happydomain.org
//
// This software is a computer program whose purpose is to provide a modern
// interface to interact with DNS systems.
//
// This software is governed by the CeCILL license under French law and abiding
// by the rules of distribution of free software. You can use, modify and/or
// redistribute the software under the terms of the CeCILL license as
// circulated by CEA, CNRS and INRIA at the following URL
// "http://www.cecill.info".
//
// As a counterpart to the access to the source code and rights to copy, modify
// and redistribute granted by the license, users are provided only with a
// limited warranty and the software's author, the holder of the economic
// rights, and the successive licensors have only limited liability.
//
// In this respect, the user's attention is drawn to the risks associated with
// loading, using, modifying and/or developing or reproducing the software by
// the user in light of its specific status of free software, that may mean
// that it is complicated to manipulate, and that also therefore means that it
// is reserved for developers and experienced professionals having in-depth
// computer knowledge. Users are therefore encouraged to load and test the
// software's suitability as regards their requirements in conditions enabling
// the security of their systems and/or data to be ensured and, more generally,
// to use and operate it in the same conditions as regards security.
//
// The fact that you are presently reading this means that you have had
// knowledge of the CeCILL license and that you accept its terms.
package happydns
import (
"time"
)
const (
LOG_CRIT = iota
LOG_FATAL
LOG_ERR2
LOG_ERR
LOG_STRONG_WARN
LOG_WARN
LOG_WEIRD
LOG_NACK
LOG_INFO
LOG_ACK
LOG_DEBUG
)
type DomainLog struct {
// Id is the Log's identifier in the database.
Id Identifier `json:"id" swaggertype:"string"`
// IdUser is the identifier of the person responsible for the action.
IdUser Identifier `json:"id_user" swaggertype:"string"`
// Date is the date of the action.
Date time.Time `json:"date"`
// Content is the description of the action logged.
Content string `json:"content"`
// Level reports the criticity level of the action logged.
Level int8 `json:"level"`
}
func NewDomainLog(u *User, level int8, content string) *DomainLog {
return &DomainLog{
IdUser: u.Id,
Date: time.Now(),
Content: content,
Level: level,
}
}

View File

@ -100,6 +100,16 @@ type Storage interface {
// ClearDomains deletes all Domains present in the database.
ClearDomains() error
// DOMAIN LOGS --------------------------------------------------
GetDomainLogs(*happydns.Domain) ([]*happydns.DomainLog, error)
CreateDomainLog(*happydns.Domain, *happydns.DomainLog) error
UpdateDomainLog(*happydns.Domain, *happydns.DomainLog) error
DeleteDomainLog(*happydns.Domain, *happydns.DomainLog) error
// PROVIDERS ----------------------------------------------------
// GetProviderMetas retrieves provider's metadatas of all providers own by the given User.

View File

@ -71,7 +71,7 @@ func NewLevelDBStorage(path string) (s *LevelDBStorage, err error) {
}
func (s *LevelDBStorage) Tidy() error {
for _, tidy := range []func() error{s.TidySessions, s.TidyAuthUsers, s.TidyUsers, s.TidyProviders, s.TidyDomains, s.TidyZones} {
for _, tidy := range []func() error{s.TidySessions, s.TidyAuthUsers, s.TidyUsers, s.TidyProviders, s.TidyDomains, s.TidyZones, s.TidyDomainLogs} {
if err := tidy(); err != nil {
return err
}

View File

@ -0,0 +1,129 @@
// Copyright or © or Copr. happyDNS (2023)
//
// contact@happydomain.org
//
// This software is a computer program whose purpose is to provide a modern
// interface to interact with DNS systems.
//
// This software is governed by the CeCILL license under French law and abiding
// by the rules of distribution of free software. You can use, modify and/or
// redistribute the software under the terms of the CeCILL license as
// circulated by CEA, CNRS and INRIA at the following URL
// "http://www.cecill.info".
//
// As a counterpart to the access to the source code and rights to copy, modify
// and redistribute granted by the license, users are provided only with a
// limited warranty and the software's author, the holder of the economic
// rights, and the successive licensors have only limited liability.
//
// In this respect, the user's attention is drawn to the risks associated with
// loading, using, modifying and/or developing or reproducing the software by
// the user in light of its specific status of free software, that may mean
// that it is complicated to manipulate, and that also therefore means that it
// is reserved for developers and experienced professionals having in-depth
// computer knowledge. Users are therefore encouraged to load and test the
// software's suitability as regards their requirements in conditions enabling
// the security of their systems and/or data to be ensured and, more generally,
// to use and operate it in the same conditions as regards security.
//
// The fact that you are presently reading this means that you have had
// knowledge of the CeCILL license and that you accept its terms.
package database
import (
"fmt"
"log"
"strings"
"git.happydns.org/happyDomain/model"
"github.com/syndtr/goleveldb/leveldb/util"
)
func (s *LevelDBStorage) GetDomainLogs(domain *happydns.Domain) (logs []*happydns.DomainLog, err error) {
iter := s.search(fmt.Sprintf("domain.log|%s|", domain.Id.String()))
defer iter.Release()
for iter.Next() {
var z happydns.DomainLog
err = decodeData(iter.Value(), &z)
if err != nil {
return
}
logs = append(logs, &z)
}
return
}
func (s *LevelDBStorage) getDomainLog(id string) (l *happydns.DomainLog, d *happydns.Domain, err error) {
l = &happydns.DomainLog{}
err = s.get(id, l)
st := strings.Split(id, "|")
if len(st) < 3 {
return
}
d = &happydns.Domain{}
err = s.get(id, fmt.Sprintf("domain-%s", st[1]))
return
}
func (s *LevelDBStorage) CreateDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
key, id, err := s.findIdentifierKey(fmt.Sprintf("domain.log|%s|", d.Id.String()))
if err != nil {
return err
}
l.Id = id
return s.put(key, l)
}
func (s *LevelDBStorage) UpdateDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
return s.put(fmt.Sprintf("domain.log|%s|%s", d.Id.String(), l.Id.String()), l)
}
func (s *LevelDBStorage) DeleteDomainLog(d *happydns.Domain, l *happydns.DomainLog) error {
return s.delete(fmt.Sprintf("domain.log|%s|%s", d.Id.String(), l.Id.String()))
}
func (s *LevelDBStorage) TidyDomainLogs() error {
tx, err := s.db.OpenTransaction()
if err != nil {
return err
}
iter := tx.NewIterator(util.BytesPrefix([]byte("domain.log|")), nil)
defer iter.Release()
for iter.Next() {
l, _, err := s.getDomainLog(string(iter.Key()))
if err != nil {
if l != nil {
log.Printf("Deleting log without valid domain: %s (%s)\n", l.Id.String(), err.Error())
} else {
log.Printf("Deleting unreadable log (%s): %v\n", err.Error(), l)
}
err = tx.Delete(iter.Key(), nil)
}
if err != nil {
tx.Discard()
return err
}
}
err = tx.Commit()
if err != nil {
tx.Discard()
return err
}
return nil
}

View File

@ -1,5 +1,5 @@
import { handleEmptyApiResponse, handleApiResponse } from '$lib/errors';
import type { Domain, DomainInList } from '$lib/model/domain';
import type { Domain, DomainInList, DomainLog } from '$lib/model/domain';
import type { Provider } from '$lib/model/provider';
export async function listDomains(): Promise<Array<DomainInList>> {
@ -43,3 +43,9 @@ export async function deleteDomain(id: string): Promise<boolean> {
});
return await handleEmptyApiResponse(res);
}
export async function getDomainLogs(id: string): Promise<Array<DomainLog>> {
id = encodeURIComponent(id);
const res = await fetch(`/api/domains/${id}/logs`, {headers: {'Accept': 'application/json'}});
return await handleApiResponse<Array<DomainLog>>(res);
}

View File

@ -29,3 +29,11 @@ export interface Domain {
// interface property
wait: boolean;
};
export interface DomainLog {
id: string;
id_user: string;
date: Date;
content: string;
level: number;
};

View File

@ -0,0 +1,54 @@
<script lang="ts">
import {
Button,
Icon,
Table,
Spinner,
} from 'sveltestrap';
import { getDomainLogs } from '$lib/api/domains';
import { t } from '$lib/translations';
export let data: {domain: DomainInList; history: string; streamed: Object;};
</script>
<div class="flex-fill pb-4 pt-2">
<h2>Journal du domaine <span class="font-monospace">{data.domain.domain}</span></h2>
{#await getDomainLogs(data.domain.id)}
<div class="mt-5 text-center flex-fill">
<Spinner label="Spinning" />
<p>{$t('wait.loading')}</p>
</div>
{:then logs}
<Table hover stripped>
<thead>
<tr>
<th>Utilisateur</th>
<th>Action/description</th>
<th>Date</th>
<th>Niveau</th>
</tr>
</thead>
<tbody>
{#if logs}
{#each logs as log}
<tr>
<td>{log.id_user}</td>
<td>{log.content}</td>
<td>
{new Intl.DateTimeFormat(undefined, {dateStyle: "short", timeStyle: "short"}).format(log.date)}
</td>
<td>{log.level}</td>
</tr>
{/each}
{:else}
<tr>
<td colspan="4" class="text-center">
Aucune entrée dans le journal du domaine.
</td>
</tr>
{/if}
</tbody>
</Table>
{/await}
</div>

View File

@ -0,0 +1,10 @@
import type { Load } from '@sveltejs/kit';
export const load: Load = async({ parent }) => {
const data = await parent();
return {
...data,
isAuditPage: true,
}
}