292 lines
9.4 KiB
Go
292 lines
9.4 KiB
Go
package issuance
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"git.apinb.com/ops/license/sdk/licence"
|
|
)
|
|
|
|
type Repository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
type issuanceRow struct {
|
|
ID uuid.UUID `gorm:"column:id;type:uuid;primaryKey"`
|
|
SubjectID uuid.UUID `gorm:"column:subject_id;type:uuid"`
|
|
IssuanceType string `gorm:"column:issuance_type"`
|
|
PreviousLicenceID *uuid.UUID `gorm:"column:previous_licence_id;type:uuid"`
|
|
ValidFrom time.Time `gorm:"column:valid_from"`
|
|
ExpiresOn time.Time `gorm:"column:expires_on"`
|
|
MaxDatabase int64 `gorm:"column:max_database"`
|
|
MaxMiddleware int64 `gorm:"column:max_middleware"`
|
|
MaxNetworkDevice int64 `gorm:"column:max_network_device"`
|
|
MaxSecurity int64 `gorm:"column:max_security"`
|
|
MaxStorage int64 `gorm:"column:max_storage"`
|
|
MaxPC int64 `gorm:"column:max_pc"`
|
|
MaxServer int64 `gorm:"column:max_server"`
|
|
MaxUser int64 `gorm:"column:max_user"`
|
|
MaxRole int64 `gorm:"column:max_role"`
|
|
MaxPermission int64 `gorm:"column:max_permission"`
|
|
MaxMenu int64 `gorm:"column:max_menu"`
|
|
SigningKeyID uuid.UUID `gorm:"column:signing_key_id;type:uuid"`
|
|
PayloadJSON []byte `gorm:"column:payload_json;type:jsonb"`
|
|
FileContent []byte `gorm:"column:file_content"`
|
|
IssuedAt time.Time `gorm:"column:issued_at"`
|
|
OperatorName string `gorm:"column:operator_name"`
|
|
}
|
|
|
|
type summaryRow struct {
|
|
ID uuid.UUID `gorm:"column:id"`
|
|
SubjectID uuid.UUID `gorm:"column:subject_id"`
|
|
PlatformName string `gorm:"column:platform_name"`
|
|
Workspace string `gorm:"column:workspace"`
|
|
IssuanceType string `gorm:"column:issuance_type"`
|
|
PreviousLicenceID *uuid.UUID `gorm:"column:previous_licence_id"`
|
|
ValidFrom time.Time `gorm:"column:valid_from"`
|
|
ExpiresOn time.Time `gorm:"column:expires_on"`
|
|
IssuedAt time.Time `gorm:"column:issued_at"`
|
|
}
|
|
|
|
type downloadRow struct {
|
|
ID uuid.UUID `gorm:"column:id"`
|
|
PlatformName string `gorm:"column:platform_name"`
|
|
Workspace string `gorm:"column:workspace"`
|
|
FileContent []byte `gorm:"column:file_content"`
|
|
}
|
|
|
|
func (issuanceRow) TableName() string {
|
|
return "licence_issuances"
|
|
}
|
|
|
|
func NewRepository(db *gorm.DB) (*Repository, error) {
|
|
if db == nil {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
return &Repository{db: db}, nil
|
|
}
|
|
|
|
func (r *Repository) Insert(ctx context.Context, tx *gorm.DB, record Record) error {
|
|
if r == nil || r.db == nil || ctx == nil || tx == nil {
|
|
return ErrInvalidInput
|
|
}
|
|
payload, err := json.Marshal(record.Payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row := rowFromRecord(record, payload)
|
|
if err := tx.WithContext(ctx).Create(&row).Error; err != nil {
|
|
return mapDatabaseError(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) Get(ctx context.Context, id uuid.UUID) (Record, error) {
|
|
var row issuanceRow
|
|
err := r.db.WithContext(ctx).Where("id = ?", id).Take(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return Record{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return row.record()
|
|
}
|
|
|
|
func (r *Repository) List(ctx context.Context, filter Filter, page, pageSize int) ([]RecordSummary, int64, error) {
|
|
query := r.db.WithContext(ctx).
|
|
Table("licence_issuances AS issuances").
|
|
Joins("JOIN authorization_subjects AS subjects ON subjects.id = issuances.subject_id")
|
|
if filter.PlatformName != "" {
|
|
query = query.Where("STRPOS(LOWER(subjects.platform_name), LOWER(?)) > 0", filter.PlatformName)
|
|
}
|
|
if filter.Workspace != "" {
|
|
query = query.Where("STRPOS(LOWER(subjects.workspace), LOWER(?)) > 0", filter.Workspace)
|
|
}
|
|
if filter.ValidFrom != nil {
|
|
query = query.Where("issuances.valid_from = ?", *filter.ValidFrom)
|
|
}
|
|
if filter.ExpiresOn != nil {
|
|
query = query.Where("issuances.expires_on = ?", *filter.ExpiresOn)
|
|
}
|
|
if filter.IssuedOn != nil {
|
|
query = query.Where("issuances.issued_at >= ? AND issuances.issued_at < ?", *filter.IssuedOn, filter.IssuedOn.AddDate(0, 0, 1))
|
|
}
|
|
return listSummaries(query, page, pageSize)
|
|
}
|
|
|
|
func (r *Repository) ListBySubject(ctx context.Context, subjectID uuid.UUID, page, pageSize int) ([]RecordSummary, int64, error) {
|
|
query := r.db.WithContext(ctx).
|
|
Table("licence_issuances AS issuances").
|
|
Joins("JOIN authorization_subjects AS subjects ON subjects.id = issuances.subject_id").
|
|
Where("issuances.subject_id = ?", subjectID)
|
|
return listSummaries(query, page, pageSize)
|
|
}
|
|
|
|
func (r *Repository) Download(ctx context.Context, id uuid.UUID) ([]byte, DownloadMetadata, error) {
|
|
var row downloadRow
|
|
err := r.db.WithContext(ctx).
|
|
Table("licence_issuances AS issuances").
|
|
Select("issuances.id, subjects.platform_name, subjects.workspace, issuances.file_content").
|
|
Joins("JOIN authorization_subjects AS subjects ON subjects.id = issuances.subject_id").
|
|
Where("issuances.id = ?", id).
|
|
Take(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, DownloadMetadata{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, DownloadMetadata{}, err
|
|
}
|
|
return cloneBytes(row.FileContent), DownloadMetadata{
|
|
ID: row.ID,
|
|
PlatformName: row.PlatformName,
|
|
Workspace: row.Workspace,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Repository) lock(ctx context.Context, tx *gorm.DB, id uuid.UUID) (Record, error) {
|
|
var row issuanceRow
|
|
err := tx.WithContext(ctx).
|
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("id = ?", id).
|
|
Take(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return Record{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
return row.record()
|
|
}
|
|
|
|
func listSummaries(query *gorm.DB, page, pageSize int) ([]RecordSummary, int64, error) {
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var rows []summaryRow
|
|
err := query.
|
|
Select(`issuances.id, issuances.subject_id, subjects.platform_name, subjects.workspace,
|
|
issuances.issuance_type, issuances.previous_licence_id, issuances.valid_from,
|
|
issuances.expires_on, issuances.issued_at`).
|
|
Order("issuances.issued_at DESC, issuances.id DESC").
|
|
Offset((page - 1) * pageSize).
|
|
Limit(pageSize).
|
|
Find(&rows).Error
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items := make([]RecordSummary, len(rows))
|
|
for index := range rows {
|
|
items[index] = rows[index].summary()
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func rowFromRecord(record Record, payload []byte) issuanceRow {
|
|
return issuanceRow{
|
|
ID: record.ID,
|
|
SubjectID: record.SubjectID,
|
|
IssuanceType: record.IssuanceType,
|
|
PreviousLicenceID: cloneUUID(record.PreviousLicenceID),
|
|
ValidFrom: record.ValidFrom,
|
|
ExpiresOn: record.ExpiresOn,
|
|
MaxDatabase: record.Quotas.MaxDatabase,
|
|
MaxMiddleware: record.Quotas.MaxMiddleware,
|
|
MaxNetworkDevice: record.Quotas.MaxNetworkDevice,
|
|
MaxSecurity: record.Quotas.MaxSecurity,
|
|
MaxStorage: record.Quotas.MaxStorage,
|
|
MaxPC: record.Quotas.MaxPC,
|
|
MaxServer: record.Quotas.MaxServer,
|
|
MaxUser: record.Quotas.MaxUser,
|
|
MaxRole: record.Quotas.MaxRole,
|
|
MaxPermission: record.Quotas.MaxPermission,
|
|
MaxMenu: record.Quotas.MaxMenu,
|
|
SigningKeyID: record.SigningKeyID,
|
|
PayloadJSON: cloneBytes(payload),
|
|
FileContent: cloneBytes(record.FileContent),
|
|
IssuedAt: record.IssuedAt,
|
|
OperatorName: record.OperatorName,
|
|
}
|
|
}
|
|
|
|
func (row issuanceRow) record() (Record, error) {
|
|
var payload licence.Claims
|
|
if err := json.Unmarshal(row.PayloadJSON, &payload); err != nil {
|
|
return Record{}, err
|
|
}
|
|
return Record{
|
|
ID: row.ID,
|
|
SubjectID: row.SubjectID,
|
|
IssuanceType: row.IssuanceType,
|
|
PreviousLicenceID: cloneUUID(row.PreviousLicenceID),
|
|
ValidFrom: row.ValidFrom,
|
|
ExpiresOn: row.ExpiresOn,
|
|
Quotas: licence.Quotas{
|
|
MaxDatabase: row.MaxDatabase,
|
|
MaxMiddleware: row.MaxMiddleware,
|
|
MaxNetworkDevice: row.MaxNetworkDevice,
|
|
MaxSecurity: row.MaxSecurity,
|
|
MaxStorage: row.MaxStorage,
|
|
MaxPC: row.MaxPC,
|
|
MaxServer: row.MaxServer,
|
|
MaxUser: row.MaxUser,
|
|
MaxRole: row.MaxRole,
|
|
MaxPermission: row.MaxPermission,
|
|
MaxMenu: row.MaxMenu,
|
|
},
|
|
SigningKeyID: row.SigningKeyID,
|
|
Payload: payload,
|
|
FileContent: cloneBytes(row.FileContent),
|
|
IssuedAt: row.IssuedAt,
|
|
OperatorName: row.OperatorName,
|
|
}, nil
|
|
}
|
|
|
|
func (row summaryRow) summary() RecordSummary {
|
|
return RecordSummary{
|
|
ID: row.ID,
|
|
SubjectID: row.SubjectID,
|
|
PlatformName: row.PlatformName,
|
|
Workspace: row.Workspace,
|
|
IssuanceType: row.IssuanceType,
|
|
PreviousLicenceID: cloneUUID(row.PreviousLicenceID),
|
|
ValidFrom: row.ValidFrom,
|
|
ExpiresOn: row.ExpiresOn,
|
|
IssuedAt: row.IssuedAt,
|
|
}
|
|
}
|
|
|
|
func mapDatabaseError(err error) error {
|
|
var postgresError *pgconn.PgError
|
|
if errors.As(err, &postgresError) && postgresError.Code == "23503" &&
|
|
postgresError.ConstraintName == "licence_issuances_previous_subject_fkey" {
|
|
return ErrPreviousMismatch
|
|
}
|
|
return err
|
|
}
|
|
|
|
func cloneBytes(value []byte) []byte {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
result := make([]byte, len(value))
|
|
copy(result, value)
|
|
return result
|
|
}
|
|
|
|
func cloneUUID(value *uuid.UUID) *uuid.UUID {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
result := *value
|
|
return &result
|
|
}
|