245 lines
6.6 KiB
Go
245 lines
6.6 KiB
Go
package issuance
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
|
|
"git.apinb.com/ops/license/internal/subject"
|
|
"git.apinb.com/ops/license/sdk/licence"
|
|
)
|
|
|
|
const (
|
|
maximumOperatorRunes = 128
|
|
maximumPageSize = 100
|
|
)
|
|
|
|
type Service struct {
|
|
repository *Repository
|
|
subjectRepository *subject.Repository
|
|
signer *FileSigner
|
|
}
|
|
|
|
func NewService(repository *Repository, subjectRepository *subject.Repository, signer *FileSigner) (*Service, error) {
|
|
if repository == nil || repository.db == nil || subjectRepository == nil || signer == nil || signer.active == nil {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
return &Service{
|
|
repository: repository,
|
|
subjectRepository: subjectRepository,
|
|
signer: signer,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Issue(ctx context.Context, subjectID uuid.UUID, input IssueInput, operator string) (Record, error) {
|
|
operator, err := s.validateWrite(ctx, subjectID, operator)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
|
|
var issued Record
|
|
err = s.repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
item, err := s.subjectRepository.GetForUpdate(ctx, tx, subjectID)
|
|
if err != nil {
|
|
return mapSubjectError(err)
|
|
}
|
|
now := time.Now().UTC()
|
|
claims, content, err := s.signer.Build(item, input, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
record, err := buildRecord(item.ID, "issue", nil, claims, content, now, operator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.repository.Insert(ctx, tx, record); err != nil {
|
|
return err
|
|
}
|
|
issued = record
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
issued.FileContent = cloneBytes(issued.FileContent)
|
|
return issued, nil
|
|
}
|
|
|
|
func (s *Service) Renew(ctx context.Context, previousID uuid.UUID, input IssueInput, operator string) (Record, error) {
|
|
operator, err := s.validateWrite(ctx, previousID, operator)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
|
|
var renewed Record
|
|
err = s.repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
previous, err := s.repository.lock(ctx, tx, previousID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item, err := s.subjectRepository.GetForUpdate(ctx, tx, previous.SubjectID)
|
|
if err != nil {
|
|
return mapSubjectError(err)
|
|
}
|
|
now := time.Now().UTC()
|
|
claims, content, err := s.signer.Build(item, input, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
record, err := buildRecord(item.ID, "renewal", &previousID, claims, content, now, operator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.repository.Insert(ctx, tx, record); err != nil {
|
|
return err
|
|
}
|
|
renewed = record
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
renewed.FileContent = cloneBytes(renewed.FileContent)
|
|
return renewed, nil
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context, id uuid.UUID) (Record, error) {
|
|
if !s.usable(ctx) || id == uuid.Nil {
|
|
return Record{}, ErrInvalidInput
|
|
}
|
|
return s.repository.Get(ctx, id)
|
|
}
|
|
|
|
func (s *Service) List(ctx context.Context, filter Filter, page, pageSize int) ([]RecordSummary, int64, error) {
|
|
if !s.usable(ctx) || !validPage(page, pageSize) {
|
|
return nil, 0, ErrInvalidInput
|
|
}
|
|
normalized, err := normalizeFilter(filter)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return s.repository.List(ctx, normalized, page, pageSize)
|
|
}
|
|
|
|
func (s *Service) ListBySubject(ctx context.Context, subjectID uuid.UUID, page, pageSize int) ([]RecordSummary, int64, error) {
|
|
if !s.usable(ctx) || subjectID == uuid.Nil || !validPage(page, pageSize) {
|
|
return nil, 0, ErrInvalidInput
|
|
}
|
|
return s.repository.ListBySubject(ctx, subjectID, page, pageSize)
|
|
}
|
|
|
|
func (s *Service) Download(ctx context.Context, id uuid.UUID) ([]byte, DownloadMetadata, error) {
|
|
if !s.usable(ctx) || id == uuid.Nil {
|
|
return nil, DownloadMetadata{}, ErrInvalidInput
|
|
}
|
|
return s.repository.Download(ctx, id)
|
|
}
|
|
|
|
func (s *Service) validateWrite(ctx context.Context, id uuid.UUID, operator string) (string, error) {
|
|
if !s.usable(ctx) || id == uuid.Nil {
|
|
return "", ErrInvalidInput
|
|
}
|
|
operator = strings.TrimSpace(operator)
|
|
if operator == "" || !utf8.ValidString(operator) || utf8.RuneCountInString(operator) > maximumOperatorRunes {
|
|
return "", ErrInvalidInput
|
|
}
|
|
for _, character := range operator {
|
|
if unicode.IsControl(character) {
|
|
return "", ErrInvalidInput
|
|
}
|
|
}
|
|
return operator, nil
|
|
}
|
|
|
|
func (s *Service) usable(ctx context.Context) bool {
|
|
return s != nil && s.repository != nil && s.repository.db != nil &&
|
|
s.subjectRepository != nil && s.signer != nil && s.signer.active != nil && ctx != nil
|
|
}
|
|
|
|
func validPage(page, pageSize int) bool {
|
|
return page >= 1 && pageSize >= 1 && pageSize <= maximumPageSize
|
|
}
|
|
|
|
func normalizeFilter(filter Filter) (Filter, error) {
|
|
filter.PlatformName = strings.TrimSpace(filter.PlatformName)
|
|
filter.Workspace = strings.TrimSpace(filter.Workspace)
|
|
var err error
|
|
filter.ValidFrom, err = normalizeDateFilter(filter.ValidFrom)
|
|
if err != nil {
|
|
return Filter{}, err
|
|
}
|
|
filter.ExpiresOn, err = normalizeDateFilter(filter.ExpiresOn)
|
|
if err != nil {
|
|
return Filter{}, err
|
|
}
|
|
filter.IssuedOn, err = normalizeDateFilter(filter.IssuedOn)
|
|
if err != nil {
|
|
return Filter{}, err
|
|
}
|
|
return filter, nil
|
|
}
|
|
|
|
func normalizeDateFilter(value *time.Time) (*time.Time, error) {
|
|
if value == nil {
|
|
return nil, nil
|
|
}
|
|
if value.IsZero() {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
date, err := licence.NewDate(*value)
|
|
if err != nil {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
normalized, err := date.Time()
|
|
if err != nil {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
return &normalized, nil
|
|
}
|
|
|
|
func buildRecord(subjectID uuid.UUID, issuanceType string, previousID *uuid.UUID, claims licence.Claims, content []byte, issuedAt time.Time, operator string) (Record, error) {
|
|
id, err := uuid.Parse(claims.ID)
|
|
if err != nil {
|
|
return Record{}, fmt.Errorf("解析许可证 ID: %w", err)
|
|
}
|
|
signingKeyID, err := uuid.Parse(claims.SigningKeyID)
|
|
if err != nil {
|
|
return Record{}, fmt.Errorf("解析签发密钥 ID: %w", err)
|
|
}
|
|
validFrom, err := claims.ValidFrom.Time()
|
|
if err != nil {
|
|
return Record{}, fmt.Errorf("解析生效日期: %w", err)
|
|
}
|
|
expiresOn, err := claims.ExpiresOn.Time()
|
|
if err != nil {
|
|
return Record{}, fmt.Errorf("解析到期日期: %w", err)
|
|
}
|
|
return Record{
|
|
ID: id,
|
|
SubjectID: subjectID,
|
|
IssuanceType: issuanceType,
|
|
PreviousLicenceID: cloneUUID(previousID),
|
|
ValidFrom: validFrom,
|
|
ExpiresOn: expiresOn,
|
|
Quotas: claims.Quotas,
|
|
SigningKeyID: signingKeyID,
|
|
Payload: claims,
|
|
FileContent: cloneBytes(content),
|
|
IssuedAt: issuedAt,
|
|
OperatorName: operator,
|
|
}, nil
|
|
}
|
|
|
|
func mapSubjectError(err error) error {
|
|
if err == subject.ErrNotFound {
|
|
return ErrNotFound
|
|
}
|
|
return err
|
|
}
|