openapi: 3.1.0
info:
  title: Cadenza Management API
  summary: 'API for maintenance and distribution of Cadenza Workbook repositories.'
  version: 2.0.0
  description: |
    The Cadenza management API, in its current state, allows automation of recurring repository deployment tasks.
    
    # Errors
    
    Errors are communicated using HTTP standard status codes.
    Some endpoints also provide a more descriptive message in error responses.
    While these error responses might be somewhat human-readable, their format is subject to change and should not be relied on.

    Unless the `Accept` header contains `application/json` or `application/problem+json`, Cadenza may deem the client unwilling to receive detailed error information in the response body.
    Therefore, we strongly recommend to always include `application/problem+json` as one of the accepted representations to receive detailed error messages.
    
    # Versioning

    This API follows semantic versioning.
    Any Cadenza major release may introduce a new major version containing breaking changes.
    Cadenza patch releases may introduce new feature versions only.

    There is no plan to support multiple API versions concurrently in a single Cadenza release.

    ## Migrating from API Version 1.x

    API version 2 now requires an `If-Unmodified-Since` header for `POST` requests to the `/repositories` endpoint.
    Refer to the specific endpoint documentation for semantics and how to use it correctly.

    # Authentication
    
    Requests to the management API may be authenticated by any Cadenza-supported authenticator.
    In most situations, the API key authenticator described in the greater Cadenza developer documentation is recommended, as it fits scripting use cases best.
  contact:
    email: kontakt@disy.net
servers:
  - url: https://{hostname}/{base}/public/adminapi
    variables:
      hostname:
        default: 'localhost:8000'
      base:
        default: trunk

tags:
  - name: repository
    x-displayName: Repositories
    description: Repository maintenance operation
  - name: groups
    x-displayName: User groups and role settings
    description: User groups and role settings maintenance operation
  - name: system-settings
    x-displayName: System settings
    description: System settings from the Management Center

paths:
  /repositories:
    get:
      tags:
        - repository
      summary: List Repositories
      description: Retrieve a listing of currently-existing and accessible repositories
      operationId: listRepositories
      responses:
        '200':
          description: Repositories were found and are part of the response body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListRepositoriesResponse'
              example:
                repositories:
                  - repositoryName: b5OdARulWYW0DMC9GvsY
                    repositorySchemaId: repository-db
                    printName: My new and shiny repository
                    lastModified: "2019-08-24T14:15:22Z"
                    lastModifiedHttp: Sat, 24 Aug 2019 14:15:22 GMT
                    hasCadenzaDataStore: true
                    hasImportPrivileges: true
                  - repositoryName: Pc8YJDtHybIR3hDILuOJ
                    repositorySchemaId: repository-db
                    printName: Water bodies and where to find them in Cadenza
                    lastImported: "2025-03-05T09:22:29Z"
                    lastModified: "2025-03-05T09:22:29Z"
                    lastModifiedHttp: Wed, 24 Mar 2025 09:22:29 GMT
                    hasCadenzaDataStore: false
                    hasImportPrivileges: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags:
        - repository
      summary: 'Import repository from file'
      description: ''
      operationId: repositoryImport
      parameters:
        - $ref: '#/components/parameters/repository_schema_id'
        - name: If-Unmodified-Since
          in: header
          required: true
          schema:
            type: string
            format: http-date
            description: |
              A date/time string for use in an HTTP header, consisting of day-of-week, day-of-month, month, year, hour, minute, second, and the fixed GMT timezone.
              Cadenza accepts only the “IMF-fixdate” format defined there.
            externalDocs:
              description: MDN web docs on conditional requests
              url: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Conditional_requests#avoiding_the_lost_update_problem_with_optimistic_locking
            #externalDocs:
            #  description: RFC 9110 as provided by the HTTP working group
            #  url: https://httpwg.org/specs/rfc9110.html#http.date
          description: |
            Prevent lost updates of repositories:
            The last modification time of an existing repository should be sent that would allow overwriting the repository.
            Non-existing repositories are always considered unmodified.
            A point-in-time in the future (as defined by the server clock) may be sent to force overwriting.
          examples:
            begin-of-time:
              summary: A begin-of-time value that will prevent any overwriting.
              value: Thu, 01 Jan 1970 00:00:00 GMT
            end-of-time:
              summary: An end-of-time value that will force overwriting any existing repository.
              value: Fri, 31 Dec 9999 23:59:60 GMT
            sensible-date:
              summary: A specific date-time value, an existing repository will be overwritten if its last change was before that point in time.
              value: Thu, 20 Mar 2025 11:37:23 GMT
      requestBody:
        description: ''
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  description: 'Repository-containing zip file'
                  type: string
                  format: binary
              required:
                - file
            encoding:
              file:
                contentType: application/zip

      responses:

        '200':
          description: A pre-existing repository was successfully replaced with content from the import file.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepositoryImportResponse'
              example:
                repositoryName: uiaenrtd
                replacement: true
                missingGroups: [ a, b, c ]
        '201':
          description: A new repository was created with content from the import file.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RepositoryImportResponse'
              example:
                repositoryName: uiaenrtd
                replacement: false
                missingGroups: [ a, b, c ]

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

        '412':
          description: |
            Precondition Failed
            * The target repository has been modified after the timestamp given in the `If-Unmodified-Since` header.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              example:
                type: http://disy.net/problem/repository-modified
                title: Repository modified
                detail: The target repository has been modified after the timestamp given in the 'If-Unmodified-Since' header.

        '428':
          description: |
            Precondition Required
            * The `If-Unmodified-Since` header, which should prevent accidental overwrites of existing repositories, is missing.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              example:
                type: http://disy.net/problem/missing-if-unmodified-since
                title: Missing If-Unmodified-Since header

        '500':
          $ref: '#/components/responses/InternalServerError'

  /repositories/{repositoryName}:
    get:
      tags:
        - repository
      summary: 'Export repository as ZIP file'
      description: |-
        Export a repository as a ZIP file for transport to a different Cadenza instance or storage.
      operationId: repositoryExport
      parameters:
        - $ref: '#/components/parameters/repository_name'
        - in: query
          name: exportAuthOption
          description: |
            The repository element authorizations to be exported.

            - `none` No authorization information will be exported.
            - `groups` Only permission grants to groups will be exported
            - `groups-users` Permission grants to groups and individual users will be exported, excluding owner information.
            - `groups-users-owners` All permission grants, including owner information, will be exported.
          required: false
          schema:
            type: string
            enum:
              - none
              - groups
              - groups-users
              - groups-users-owners
            default: groups
        - in: query
          name: importerAuthorization
          description: |
            The permissions to assign on import to the importer, if the import creates the repository in the target system.

            - `owner` The importer will become the full owner of the repository, assuming most permissions on the contained items.
            - `manage-content` The importer will receive permissions to manage repository content, allowing him to list items, see their owner, and to delete content.
          required: false
          schema:
            type: string
            enum:
              - owner
              - manage-content
            default: owner
        - in: query
          name: exportStoredData
          description: |
            Whether to export the data stored in the Cadenza Data Store of the repository.
            If the repository contains no data source designated as Cadenza Data Store, the parameter has no effects.
            
            - `no-data` No data will be exported.
            - `bound-objecttype-data` Data from Objecttypes bound to workbooks in the repository will be included in the export.
            - `all-cds-objecttype-data` Data from all Objecttypes in the Cadenza Data Store of the repository will be included in the export.
          required: false
          schema:
            type: string
            enum:
              - no-data
              - bound-objecttype-data
              - all-cds-objecttype-data
            default: no-data
      responses:

        '200':
          description: successful operation
          content:
            application/zip:
              schema:
                type: string
                format: binary

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      tags:
        - repository
      summary: Delete repository
      description: ''
      operationId: repositoryDelete
      parameters:
        - $ref: '#/components/parameters/repository_name'
      responses:

        '200':
          description: successful operation

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /repositories/{repositoryName}/runtestsuite:
    post:
      tags:
        - repository
      summary: Run defined tests for repository
      description: ''
      operationId: runtestsuite
      parameters:
        - $ref: '#/components/parameters/repository_name'
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResponse'

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /groups:

    post:
      tags:
        - groups
      summary: 'Import user groups and role settings as ZIP file'
      description: |-
        Replaces all user groups and role settings from a ZIP file.
      operationId: groupsImport
      requestBody:
        description: ''
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  description: 'User groups and role settings containing zip file'
                  type: string
                  format: binary
              required:
                - file
            encoding:
              file:
                contentType: application/zip
      responses:

        '200':
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupsAndRolesImportResponse'

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      tags:
        - groups
      summary: 'Export user groups and role settings as ZIP file'
      description: |-
        Export all user groups and role settings as a ZIP file for transport to a different Cadenza instance or storage.
      operationId: groupsExport
      responses:

        '200':
          description: successful operation
          content:
            application/zip:
              schema:
                type: string
                format: binary

        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /system-settings:
    get:
      tags:
        - system-settings
      summary: 'Export system settings as a ZIP file'
      description: |-
        Export system settings from Cadenza as a ZIP file for transport to a different Cadenza instance
      operationId: systemSettingsExport
      responses:
        '200':
          description: successful operation
          content:
            application/zip:
              schema:
                type: string
                format: binary

        '401':
          $ref: '#/components/responses/Unauthorized'

        '403':
          description: Forbidden

        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - system-settings
      summary: 'Import system settings from a ZIP file'
      description: |-
        Import system settings from a ZIP file. Replaces all existing system settings in Cadenza.
      operationId: systemSettingsImport
      requestBody:
        description: ''
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  description: 'ZIP file containing Cadenza system settings'
                  type: string
                  format: binary
              required:
                - file
            encoding:
              file:
                contentType: application/zip

      responses:

        '200':
          description: System settings were successfully replaced with content from the import file.

        '400':
          description: |
            Bad request
            * The uploaded file is empty.

        '401':
          $ref: '#/components/responses/Unauthorized'

        '403':
          description: Forbidden

        '500':
          $ref: '#/components/responses/InternalServerError'

components:

  parameters:
    repository_schema_id:
      in: query
      name: repositorySchemaId
      description: |-
        Technical name of the target repository database.
        This must match a database repository in `repositoryList.xml` by `databaseRepository/@name` or the `schemaId` of a `repositorySchema` in the `databaserepository` configuration.
        The value is required when more than one repository database is defined in `repositoryList.xml` (or `databaserepository` configuration) and the repository does not exist in the target system yet.
      required: false
      schema:
        type: string
    repository_name:
      in: path
      name: repositoryName
      description: Technical repository name
      required: true
      schema:
        type: string

  responses:
    BadRequest:
      description: |
        Bad Request

        The request is malformed in a way that does not fit into one of the other client-side error categories.
        Possible reasons (non-exhaustive):
        * It might miss a required request parameter, such as a repository schema id for import.
        * A parameter value may be invalid.
        * An uploaded file is empty or does not conform to the expected format.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: http://disy.net/problem/illegal-parameter-value
            title: Illegal Parameter Value
            detail: Invalid value "'no-data'" for parameter "exportStoredData"

    Unauthorized:
      description: |
        Unauthorized
        * Authentication failed

        If using Api-Key authentication, this could mean any of
        * No valid api clients are configured
        * Missing signature parameter
        * Missing requestTimestamp parameter
        * Request is expired
        * Invalid signature
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: http://disy.net/problem/authentication-failed
            title: Authentication failed
            detail: Admin API authentication failed. Expired API key.

    Forbidden:
      description: |
        Forbidden

        The authenticated client does not have the necessary privileges to perform an operation on a resource.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: http://disy.net/problem/repository-access-denied
            title: Forbidden

    NotFound:
      description: |
        Not Found
        * Repository does not exist
        * Missing permissions to view the repository
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          example:
            type: http://disy.net/problem/repository-not-found
            title: Repository not found
            detail: Repository 'uiaenrtd' not found.

    InternalServerError:
      description: |
        Internal Server Error

        The server has encountered a problem, possibly due to configuration problems, backend system failures, or software errors.
        If the problem persists, consult server-side logs for more information about the error.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          examples:
            internalError:
              value:
                type: http://disy.net/problem/internal-error
                title: An internal error occurred.
                detail: An internal error occurred and has been logged with ID "ERR123456789".
                instance: http://disy.net/problem-instance/ERR123456789

  schemas:
    State:
      type: string
      description: Validation state
      enum:
        - OK
        - WARNING
        - ERROR

    ResourceType:
      type: string
      description: Repository resource type
      enum:
        - DATASOURCE
        - DATASOURCE_VERSION
        - BASIC_OBJECTTYPE
        - COMBINED_OBJECTTYPE
        - COMBINED_OBJECTTYPE_REFERENCE
        - OBJECTTYPE_REFERENCE
        - ATTRIBUTE_REFERENCE
        - HIERARCHY_REFERENCE
        - BASIC_OBJECTTYPE_SOURCE
        - OBJECTTYPE_ATTRIBUTE
        - WORKBOOK
        - DEFAULT_MAP
        - REPOSITORY

    Entry:
      type: object
      properties:
        resourceType:
          $ref: '#/components/schemas/ResourceType'
        description:
          type: string
        state:
          $ref: '#/components/schemas/State'

    ProblemDetails:
      type: object
      externalDocs:
        description: IETF RFC 9457 Problem Details for HTTP APIs
        url: https://datatracker.ietf.org/doc/rfc9457/
      properties:
        type:
          type: string
          format: uri
          description: |
            The primary identifier for the experienced problem type.
            As it is meant as an identifier, it is typically not navigable.
        status:
          type: integer
          description: |
            The corresponding HTTP status code.
            When present, it must match the response status.
        title:
          type: string
          description: |
            A short, human-readable summary of the problem type.
            For a given type, the title may only vary for localization.
        detail:
          type: string
          description: |
            A human-readable explanation specific to the occurrence of the problem.
            The detail text should not be parsed to extract additional information.
        instance:
          type: string
          format: uri
          description: |
            An identifier for the specific problem occurrence.
            As it is meant as an identifier, it is typically not navigable.
      required:
        - type
        - title

    ListRepositoriesResponse:
      type: object
      properties:
        repositories:
          type: array
          items:
            type: object
            properties:
              repositoryName:
                description: Technical repository name
                type: string
              repositorySchemaId:
                description: Technical name of the storage location of the repository.
                type: string
              printName:
                description: User-visible name
                type: string
              lastImported:
                description: Date and time of the last successful import, in ISO8601-based format according to RFC3339
                type: string
                format: date-time
              lastModified:
                description: Date and time of last modification to the repository, in ISO8601-based format according to RFC3339
                type: string
                format: date-time
              lastModifiedHttpDate:
                description: |
                  Date and time of last modification to the repository, in HTTP “IMF-fixdate” format for use in an `If-Unmodified-Since` header.
                type: string
                format: http-date
              hasCadenzaDataStore:
                description: Whether the repository contains a data source marked as its Cadenza Data Store.
                type: boolean
                default: false
              hasImportPrivileges:
                description: Whether the current credentials would allow overwriting the repository.
                type: boolean
                default: false
            required:
              - repositoryName
              - repositorySchemaId
              - printName
              - lastModified
              - lastModifiedHttpDate

    ValidationResponse:
      type: object
      properties:
        repositoryName:
          description: Technical repository name
          type: string
        state:
          $ref: '#/components/schemas/State'
        entries:
          type: array
          items:
            $ref: '#/components/schemas/Entry'

    RepositoryImportResponse:
      type: object
      properties:
        repositoryName:
          description: Technical repository name
          type: string
        replacement:
          type: boolean
          example: false
        missingGroups:
          description: 'List of user groups referenced in the imported repository, but not present in the target system'
          type: array
          items:
            type: string

    GroupsAndRolesImportResponse:
      type: object
      properties:
        userGroupsCount:
          description: Numbers of user groups created
          type: int
        systemRolesCount:
          description: Numbers of roles created
          type: int
        groupVariablesCount:
          description: Numbers of variables created
          type: int
