mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-13 11:34:26 +00:00
ed4983d3fd
* fix(emitter): only emit if there are log records * fix(actions): marshal invalid metadata value into string
163 lines
4.7 KiB
Go
163 lines
4.7 KiB
Go
package access
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/zitadel/logging"
|
|
"google.golang.org/grpc/codes"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/call"
|
|
zitadel_http "github.com/zitadel/zitadel/internal/api/http"
|
|
"github.com/zitadel/zitadel/internal/database"
|
|
caos_errors "github.com/zitadel/zitadel/internal/errors"
|
|
"github.com/zitadel/zitadel/internal/logstore"
|
|
"github.com/zitadel/zitadel/internal/repository/quota"
|
|
)
|
|
|
|
const (
|
|
accessLogsTable = "logstore.access"
|
|
accessTimestampCol = "log_date"
|
|
accessProtocolCol = "protocol"
|
|
accessRequestURLCol = "request_url"
|
|
accessResponseStatusCol = "response_status"
|
|
accessRequestHeadersCol = "request_headers"
|
|
accessResponseHeadersCol = "response_headers"
|
|
accessInstanceIdCol = "instance_id"
|
|
accessProjectIdCol = "project_id"
|
|
accessRequestedDomainCol = "requested_domain"
|
|
accessRequestedHostCol = "requested_host"
|
|
)
|
|
|
|
var _ logstore.UsageQuerier = (*databaseLogStorage)(nil)
|
|
var _ logstore.LogCleanupper = (*databaseLogStorage)(nil)
|
|
|
|
type databaseLogStorage struct {
|
|
dbClient *database.DB
|
|
}
|
|
|
|
func NewDatabaseLogStorage(dbClient *database.DB) *databaseLogStorage {
|
|
return &databaseLogStorage{dbClient: dbClient}
|
|
}
|
|
|
|
func (l *databaseLogStorage) QuotaUnit() quota.Unit {
|
|
return quota.RequestsAllAuthenticated
|
|
}
|
|
|
|
func (l *databaseLogStorage) Emit(ctx context.Context, bulk []logstore.LogRecord) error {
|
|
if len(bulk) == 0 {
|
|
return nil
|
|
}
|
|
builder := squirrel.Insert(accessLogsTable).
|
|
Columns(
|
|
accessTimestampCol,
|
|
accessProtocolCol,
|
|
accessRequestURLCol,
|
|
accessResponseStatusCol,
|
|
accessRequestHeadersCol,
|
|
accessResponseHeadersCol,
|
|
accessInstanceIdCol,
|
|
accessProjectIdCol,
|
|
accessRequestedDomainCol,
|
|
accessRequestedHostCol,
|
|
).
|
|
PlaceholderFormat(squirrel.Dollar)
|
|
|
|
for idx := range bulk {
|
|
item := bulk[idx].(*Record)
|
|
builder = builder.Values(
|
|
item.LogDate,
|
|
item.Protocol,
|
|
item.RequestURL,
|
|
item.ResponseStatus,
|
|
item.RequestHeaders,
|
|
item.ResponseHeaders,
|
|
item.InstanceID,
|
|
item.ProjectID,
|
|
item.RequestedDomain,
|
|
item.RequestedHost,
|
|
)
|
|
}
|
|
|
|
stmt, args, err := builder.ToSql()
|
|
if err != nil {
|
|
return caos_errors.ThrowInternal(err, "ACCESS-KOS7I", "Errors.Internal")
|
|
}
|
|
|
|
result, err := l.dbClient.ExecContext(ctx, stmt, args...)
|
|
if err != nil {
|
|
return caos_errors.ThrowInternal(err, "ACCESS-alnT9", "Errors.Access.StorageFailed")
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return caos_errors.ThrowInternal(err, "ACCESS-7KIpL", "Errors.Internal")
|
|
}
|
|
|
|
logging.WithFields("rows", rows).Debug("successfully stored access logs")
|
|
return nil
|
|
}
|
|
|
|
func (l *databaseLogStorage) QueryUsage(ctx context.Context, instanceId string, start time.Time) (uint64, error) {
|
|
stmt, args, err := squirrel.Select(
|
|
fmt.Sprintf("count(%s)", accessInstanceIdCol),
|
|
).
|
|
From(accessLogsTable + l.dbClient.Timetravel(call.Took(ctx))).
|
|
Where(squirrel.And{
|
|
squirrel.Eq{accessInstanceIdCol: instanceId},
|
|
squirrel.GtOrEq{accessTimestampCol: start},
|
|
squirrel.Expr(fmt.Sprintf(`%s #>> '{%s,0}' = '[REDACTED]'`, accessRequestHeadersCol, strings.ToLower(zitadel_http.Authorization))),
|
|
squirrel.NotLike{accessRequestURLCol: "%/zitadel.system.v1.SystemService/%"},
|
|
squirrel.NotLike{accessRequestURLCol: "%/system/v1/%"},
|
|
squirrel.Or{
|
|
squirrel.And{
|
|
squirrel.Eq{accessProtocolCol: HTTP},
|
|
squirrel.NotEq{accessResponseStatusCol: http.StatusForbidden},
|
|
squirrel.NotEq{accessResponseStatusCol: http.StatusInternalServerError},
|
|
squirrel.NotEq{accessResponseStatusCol: http.StatusTooManyRequests},
|
|
},
|
|
squirrel.And{
|
|
squirrel.Eq{accessProtocolCol: GRPC},
|
|
squirrel.NotEq{accessResponseStatusCol: codes.PermissionDenied},
|
|
squirrel.NotEq{accessResponseStatusCol: codes.Internal},
|
|
squirrel.NotEq{accessResponseStatusCol: codes.ResourceExhausted},
|
|
},
|
|
},
|
|
}).
|
|
PlaceholderFormat(squirrel.Dollar).
|
|
ToSql()
|
|
|
|
if err != nil {
|
|
return 0, caos_errors.ThrowInternal(err, "ACCESS-V9Sde", "Errors.Internal")
|
|
}
|
|
|
|
var count uint64
|
|
if err = l.dbClient.
|
|
QueryRowContext(ctx, stmt, args...).
|
|
Scan(&count); err != nil {
|
|
return 0, caos_errors.ThrowInternal(err, "ACCESS-pBPrM", "Errors.Logstore.Access.ScanFailed")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (l *databaseLogStorage) Cleanup(ctx context.Context, keep time.Duration) error {
|
|
stmt, args, err := squirrel.Delete(accessLogsTable).
|
|
Where(squirrel.LtOrEq{accessTimestampCol: time.Now().Add(-keep)}).
|
|
PlaceholderFormat(squirrel.Dollar).
|
|
ToSql()
|
|
|
|
if err != nil {
|
|
return caos_errors.ThrowInternal(err, "ACCESS-2oTh6", "Errors.Internal")
|
|
}
|
|
|
|
execCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
_, err = l.dbClient.ExecContext(execCtx, stmt, args...)
|
|
return err
|
|
}
|