Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/rest-apis/platform-api/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ login endpoint described under [Obtaining a token](#obtaining-a-token)
|ap:application:manage|Full access to applications|
|ap:application:read|Read applications|
|ap:application:update|Update an application|
|ap:docs:manage|Full access to API documents|
|ap:docs:read|Read API documents|
|ap:gateway:create|Create a gateway|
|ap:gateway:delete|Delete a gateway|
|ap:gateway:manage|Full access to gateways|
Expand Down Expand Up @@ -365,3 +367,5 @@ login endpoint described under [Obtaining a token](#obtaining-a-token)
|ap:subscription_plan:manage|Full access to subscription plans|
|ap:subscription_plan:read|Read subscription plans|
|ap:subscription_plan:update|Update a subscription plan|
|ap:thumbnail:manage|Full access to API thumbnails|
|ap:thumbnail:read|Read API thumbnails|
2 changes: 2 additions & 0 deletions kubernetes/helm/platform-api-helm-chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ config:
# Ownership override — every user's API keys, not just the caller's.
# Never implied by ap:api_key:read, so it is listed explicitly.
- ap:api_key:all:manage
- ap:docs:manage
- ap:thumbnail:manage
# Claim-name mappings shared by all modes.
claimMappings:
organization: organization
Expand Down
103 changes: 103 additions & 0 deletions platform-api/api/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions platform-api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ type Server struct {
// upload. Kept separate from PublicationContentMaxBytes — a thumbnail is a small
// icon, not a spec document, so it gets its own, tighter default (2 MiB) when <= 0.
PublicationThumbnailMaxBytes int64 `koanf:"publication_thumbnail_max_bytes"`
// ThumbnailMaxFetchBytes bounds a single-artifact thumbnail upload.
ThumbnailMaxFetchBytes int64 `koanf:"thumbnail_max_fetch_bytes"`
// AgentCardMaxFetchBytes bounds the body read from an upstream agent's Agent Card
// endpoint (internal/utils/agent_card.go). <= 0 falls back to the fetcher's built-in
// 1 MiB default, which is the contract's per-card ceiling — mirroring
Expand Down
6 changes: 6 additions & 0 deletions platform-api/internal/apperror/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,9 @@ var (
APIPublicationDraftChanged = def(CodeAPIPublicationDraftChanged, http.StatusConflict,
"The draft changed while publishing. Review it and publish again.")
)

// API document entries.
var (
APIDocumentNameExists = def(CodeAPIDocumentNameExists, http.StatusConflict,
"A document with this name already exists for this API.")
)
5 changes: 5 additions & 0 deletions platform-api/internal/apperror/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ const (
CodeArtifactDeployed = "ARTIFACT_DEPLOYED"
)

// API document domain codes.
const (
CodeAPIDocumentNameExists = "API_DOCUMENT_NAME_EXISTS"
)

// Custom policy domain codes.
const (
CodeCustomPolicyNotFound = "CUSTOM_POLICY_NOT_FOUND"
Expand Down
70 changes: 69 additions & 1 deletion platform-api/internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

package constants

import "regexp"
import (
"regexp"
"strings"
)

// SecretPlaceholderRe matches {{ secret "handle" }} (and the escaped-quote variant
// {{ secret \"handle\" }}) in artifact config blobs. A single definition here ensures
Expand Down Expand Up @@ -183,6 +186,17 @@ const (
// keep it out of self-service/developer roles.
const ScopeAPIKeyAllManage = "ap:api_key:all:manage"

// ScopeDocsRead and ScopeDocsManage govern the /apis/{apiType}/{apiId}/docs endpoints.
const (
ScopeDocsRead = "ap:docs:read"
ScopeDocsManage = "ap:docs:manage"
)

const (
ScopeThumbnailRead = "ap:thumbnail:read"
ScopeThumbnailManage = "ap:thumbnail:manage"
)

// Custom Policy ManagedBy constants
const (
PolicyManagedByOrganization = "organization"
Expand Down Expand Up @@ -299,6 +313,9 @@ var ValidThrottleLimitUnits = map[string]bool{
// upload or fetch when OpenAPISpecMaxFetchBytes is not set in config.
const DefaultOpenAPISpecMaxBytes int64 = 5 << 20 // 5 MiB

// DefaultThumbnailMaxBytes bounds a single thumbnail upload.
const DefaultThumbnailMaxBytes int64 = 1 << 20 // 1 MiB

// DefaultOpenAPISpecFileName is the filename persisted for a spec that was
// fetched by URL but whose URL has no usable last path segment to name the
// file after.
Expand All @@ -313,6 +330,57 @@ const (
DocumentDisplayNameDefinition = "OpenAPI Definition"
)

const (
DocumentTypeThumbnail = "THUMBNAIL"
DocumentHandleThumbnail = "api-thumbnail"
DocumentDisplayNameThumbnail = "API Thumbnail"
)

const (
DocumentTypeHowTo = "HowTo"
DocumentTypeSamples = "Samples"
DocumentTypeSupportForum = "SupportForum"
DocumentTypePublicForum = "PublicForum"
DocumentTypeOther = "Other"
DocumentTypePrefix = "DOC_"
)

var ValidAPIDocumentUserTypes = map[string]bool{
DocumentTypeHowTo: true,
DocumentTypeSamples: true,
DocumentTypeSupportForum: true,
DocumentTypePublicForum: true,
DocumentTypeOther: true,
}

// Fixed types (HowTo, Samples, …) are stored with the DOC_ prefix to be compatible with the api-portal.
var FixedAPIDocumentStoredTypes = []string{
DocumentTypePrefix + DocumentTypeHowTo,
DocumentTypePrefix + DocumentTypeSamples,
DocumentTypePrefix + DocumentTypeSupportForum,
DocumentTypePrefix + DocumentTypePublicForum,
}

var ReservedAPIDocumentTypes = []string{
DocumentTypeDefinition,
DocumentTypeThumbnail,
}

var ReservedAPIDocumentHandles = map[string]bool{
DocumentHandleDefinition: true,
DocumentHandleThumbnail: true,
}

var ForbiddenOtherTypeNames = map[string]bool{
strings.ToLower(DocumentTypeDefinition): true,
strings.ToLower(DocumentTypeThumbnail): true,
strings.ToLower(DocumentTypeHowTo): true,
strings.ToLower(DocumentTypeSamples): true,
strings.ToLower(DocumentTypeSupportForum): true,
strings.ToLower(DocumentTypePublicForum): true,
strings.ToLower(DocumentTypeOther): true,
}

// Metadata key constants for deployment metadata
const (
// MetadataKeyEndpointUrl is the metadata key for the per-deployment endpoint URL override.
Expand Down
36 changes: 15 additions & 21 deletions platform-api/internal/dto/api_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,22 @@

package dto

// CreateAPIDocumentRequest carries the raw spec and metadata when persisting a new spec
// document for an API. The service fills in document type, handle, display name, and content type.
// CreateAPIDocumentRequest carries the raw spec and metadata when creating or upserting a
// document for an API. The service fills in the content type.
type CreateAPIDocumentRequest struct {
Type string
Handle string
DisplayName string
FileName string
Content []byte
Type string
Handle string
DisplayName string
FileName string
Content []byte
OtherTypeName string // only meaningful when Type == "Other"; stored as-is in the type column
}

// PutAPIDocumentRequest carries the raw spec and metadata when replacing an existing
// spec document for an API. The service fills in document type, handle, display name, and content type.
type PutAPIDocumentRequest struct {
Type string
Handle string
DisplayName string
FileName string
Content []byte
}

// APIDocumentContent is returned by GetDocument — the raw spec bytes ready to serve.
type APIDocumentContent struct {
Content []byte
ContentType string
type UpdateAPIDocumentRequest struct {
Comment thread
NethmiRanasinghe marked this conversation as resolved.
Type *string // nil = leave unchanged; pointer to "" is rejected
OtherTypeName string // only meaningful when Type == "Other"
DisplayName *string
FileName *string
Content []byte
ContentType *string
}
22 changes: 12 additions & 10 deletions platform-api/internal/handler/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,13 +431,15 @@ func (h *APIHandler) GetOpenAPISpec(w http.ResponseWriter, r *http.Request) erro
return serviceError(err, "failed to resolve API "+restApiId+" in org "+orgId)
}

// Retrieve document
doc, err := h.apiDocumentService.GetDocument(artifactUUID, orgId)
// Retrieve document — strict match on handle AND type so a user doc that
// somehow registered at the reserved handle can't be returned here.
_, contentBytes, err := h.apiDocumentService.GetDocumentWithContent(artifactUUID, constants.DocumentHandleDefinition, orgId,
constants.DocumentTypeDefinition)
if err != nil {
return serviceError(err, "failed to fetch openapi spec for API "+restApiId)
}

content := string(doc.Content)
content := string(contentBytes)
httputil.WriteJSON(w, http.StatusOK, api.OpenAPIContent{Content: &content})
return nil
}
Expand Down Expand Up @@ -510,15 +512,15 @@ func (h *APIHandler) PutOpenAPISpec(w http.ResponseWriter, r *http.Request) erro
}

// Update document
docReq := &dto.PutAPIDocumentRequest{
Type: constants.DocumentTypeDefinition,
Handle: constants.DocumentHandleDefinition,
DisplayName: constants.DocumentDisplayNameDefinition,
FileName: specFileName,
Content: specContent,
docReq := &dto.CreateAPIDocumentRequest{
Type: constants.DocumentTypeDefinition,
Handle: constants.DocumentHandleDefinition,
DisplayName: constants.DocumentDisplayNameDefinition,
FileName: specFileName,
Content: specContent,
}

if err := h.apiDocumentService.PutDocument(docReq, orgId, updatedBy, artifactUUID); err != nil {
if err := h.apiDocumentService.UpsertDocument(docReq, orgId, updatedBy, artifactUUID); err != nil {
h.slogger.Error("Failed to persist spec", "api", restApiId, "error", err)
if operationsUpdated {
if _, rollbackErr := h.apiService.UpdateAPIByHandle(restApiId, existingAPI, orgId, updatedBy); rollbackErr != nil {
Expand Down
Loading
Loading