package httpapi import ( "fmt" "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "git.apinb.com/ops/license/internal/issuance" "git.apinb.com/ops/license/internal/subject" "git.apinb.com/ops/license/sdk/licence" ) const timeLayout = time.RFC3339Nano type issuanceHandler struct { service *issuance.Service subjectService *subject.Service } type issueRequest struct { ValidFrom string `json:"valid_from"` ExpiresOn string `json:"expires_on"` Quotas quotaRequest `json:"quotas"` } type quotaRequest struct { MaxDatabase *int64 `json:"max_database"` MaxMiddleware *int64 `json:"max_middleware"` MaxNetworkDevice *int64 `json:"max_network_device"` MaxSecurity *int64 `json:"max_security"` MaxStorage *int64 `json:"max_storage"` MaxPC *int64 `json:"max_pc"` MaxServer *int64 `json:"max_server"` MaxUser *int64 `json:"max_user"` MaxRole *int64 `json:"max_role"` MaxPermission *int64 `json:"max_permission"` MaxMenu *int64 `json:"max_menu"` } type issuanceSummaryView struct { ID uuid.UUID `json:"id"` SubjectID uuid.UUID `json:"subject_id"` PlatformName string `json:"platform_name"` Workspace string `json:"workspace"` IssuanceType string `json:"issuance_type"` PreviousLicenceID *uuid.UUID `json:"previous_licence_id"` ValidFrom string `json:"valid_from"` ExpiresOn string `json:"expires_on"` IssuedAt string `json:"issued_at"` } type issuanceDetailView struct { issuanceSummaryView Quotas licence.Quotas `json:"quotas"` SigningKeyID uuid.UUID `json:"signing_key_id"` OperatorName string `json:"operator_name"` } func (h issuanceHandler) issue(c *gin.Context) { subjectID, ok := parseID(c) if !ok { return } input, ok := readIssueInput(c) if !ok { return } record, err := h.service.Issue(c.Request.Context(), subjectID, input, "system") if err != nil { writeError(c, err) return } writeLicence(c, record.FileContent, record.ID) } func (h issuanceHandler) renew(c *gin.Context) { previousID, ok := parseID(c) if !ok { return } input, ok := readIssueInput(c) if !ok { return } record, err := h.service.Renew(c.Request.Context(), previousID, input, "system") if err != nil { writeError(c, err) return } writeLicence(c, record.FileContent, record.ID) } func (h issuanceHandler) list(c *gin.Context) { page, pageSize, ok := parsePagination(c) if !ok { writeBadRequest(c) return } filter, ok := parseIssuanceFilter(c) if !ok { return } var items []issuance.RecordSummary var total int64 var err error if rawSubjectID, exists := c.GetQuery("subject_id"); exists { if rawSubjectID == "" || hasGeneralIssuanceFilter(c) { writeBadRequest(c) return } subjectID, parseErr := uuid.Parse(rawSubjectID) if parseErr != nil || subjectID == uuid.Nil { writeBadRequest(c) return } items, total, err = h.service.ListBySubject(c.Request.Context(), subjectID, page, pageSize) } else { items, total, err = h.service.List(c.Request.Context(), filter, page, pageSize) } if err != nil { writeError(c, err) return } views := make([]issuanceSummaryView, len(items)) for index := range items { views[index] = newIssuanceSummaryView(items[index]) } writePage(c, views, page, pageSize, total) } func (h issuanceHandler) get(c *gin.Context) { id, ok := parseID(c) if !ok { return } record, err := h.service.Get(c.Request.Context(), id) if err != nil { writeError(c, err) return } item, err := h.subjectService.Get(c.Request.Context(), record.SubjectID) if err != nil { writeError(c, err) return } summary := issuance.RecordSummary{ ID: record.ID, SubjectID: record.SubjectID, PlatformName: item.PlatformName, Workspace: item.Workspace, IssuanceType: record.IssuanceType, PreviousLicenceID: record.PreviousLicenceID, ValidFrom: record.ValidFrom, ExpiresOn: record.ExpiresOn, IssuedAt: record.IssuedAt, } writeData(c, http.StatusOK, issuanceDetailView{ issuanceSummaryView: newIssuanceSummaryView(summary), Quotas: record.Quotas, SigningKeyID: record.SigningKeyID, OperatorName: "system", }) } func (h issuanceHandler) download(c *gin.Context) { id, ok := parseID(c) if !ok { return } content, metadata, err := h.service.Download(c.Request.Context(), id) if err != nil { writeError(c, err) return } writeLicence(c, content, metadata.ID) } func readIssueInput(c *gin.Context) (issuance.IssueInput, bool) { var request issueRequest if err := decodeJSON(c, &request); err != nil { writeBadRequest(c) return issuance.IssueInput{}, false } validFrom, err := licence.ParseDate(request.ValidFrom) if err != nil { writeError(c, issuance.ErrInvalidDates) return issuance.IssueInput{}, false } expiresOn, err := licence.ParseDate(request.ExpiresOn) if err != nil { writeError(c, issuance.ErrInvalidDates) return issuance.IssueInput{}, false } validFromTime, _ := validFrom.Time() expiresOnTime, _ := expiresOn.Time() quotas, ok := request.Quotas.value() if !ok { writeError(c, issuance.ErrInvalidInput) return issuance.IssueInput{}, false } return issuance.IssueInput{ValidFrom: validFromTime, ExpiresOn: expiresOnTime, Quotas: quotas}, true } func (q quotaRequest) value() (licence.Quotas, bool) { if q.MaxDatabase == nil || q.MaxMiddleware == nil || q.MaxNetworkDevice == nil || q.MaxSecurity == nil || q.MaxStorage == nil || q.MaxPC == nil || q.MaxServer == nil || q.MaxUser == nil || q.MaxRole == nil || q.MaxPermission == nil || q.MaxMenu == nil { return licence.Quotas{}, false } return licence.Quotas{ MaxDatabase: *q.MaxDatabase, MaxMiddleware: *q.MaxMiddleware, MaxNetworkDevice: *q.MaxNetworkDevice, MaxSecurity: *q.MaxSecurity, MaxStorage: *q.MaxStorage, MaxPC: *q.MaxPC, MaxServer: *q.MaxServer, MaxUser: *q.MaxUser, MaxRole: *q.MaxRole, MaxPermission: *q.MaxPermission, MaxMenu: *q.MaxMenu, }, true } func parseIssuanceFilter(c *gin.Context) (issuance.Filter, bool) { filter := issuance.Filter{ PlatformName: c.Query("platform_name"), Workspace: c.Query("workspace"), } var ok bool if filter.ValidFrom, ok = parseOptionalDate(c, "valid_from"); !ok { return issuance.Filter{}, false } if filter.ExpiresOn, ok = parseOptionalDate(c, "expires_on"); !ok { return issuance.Filter{}, false } if filter.IssuedOn, ok = parseOptionalDate(c, "issued_on"); !ok { return issuance.Filter{}, false } return filter, true } func parseOptionalDate(c *gin.Context, name string) (*time.Time, bool) { raw, exists := c.GetQuery(name) if !exists { return nil, true } date, err := licence.ParseDate(raw) if err != nil { writeBadRequest(c) return nil, false } value, _ := date.Time() return &value, true } func hasGeneralIssuanceFilter(c *gin.Context) bool { for _, name := range []string{"platform_name", "workspace", "valid_from", "expires_on", "issued_on"} { if _, exists := c.GetQuery(name); exists { return true } } return false } func newIssuanceSummaryView(item issuance.RecordSummary) issuanceSummaryView { return issuanceSummaryView{ ID: item.ID, SubjectID: item.SubjectID, PlatformName: item.PlatformName, Workspace: item.Workspace, IssuanceType: item.IssuanceType, PreviousLicenceID: item.PreviousLicenceID, ValidFrom: item.ValidFrom.Format("2006-01-02"), ExpiresOn: item.ExpiresOn.Format("2006-01-02"), IssuedAt: item.IssuedAt.UTC().Format(timeLayout), } } func writeLicence(c *gin.Context, content []byte, id uuid.UUID) { filename := fmt.Sprintf("%s.licence.key", id) c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) c.Header("X-Licence-ID", id.String()) c.Data(http.StatusOK, "application/json", content) }