diff --git a/.github/workflows/megalinter.yml b/.github/workflows/megalinter.yml new file mode 100644 index 0000000..5e008d3 --- /dev/null +++ b/.github/workflows/megalinter.yml @@ -0,0 +1,78 @@ +--- +# MegaLinter GitHub Action configuration file +# More info at https://megalinter.io +# CAMARA Project - Github Action for Pull Reqests +# 31.01.2024 - initial version + +name: MegaLinter + +on: # yamllint disable-line rule:truthy + # Pull Requests to main + pull_request: + branches: [master, main] + +env: # Comment env block if you do not want to apply fixes + # Apply linter fixes configuration + APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) + APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) + APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + build: + name: MegaLinter + runs-on: ubuntu-latest + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR + # Remove the ones you do not need + contents: write + issues: write + pull-requests: write + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances + - name: Install Spectral + run: npm install -g @stoplight/spectral + - name: Install Spectral functions + run: npm install -g @stoplight/spectral-functions + # - name: Run spectral:oas Spectral Linting + # run: spectral lint code/API_definitions/*.yaml --verbose --ruleset .spectral.yml + # Replace openapi.yaml file with your API specification file + + # MegaLinter + - name: MegaLinter + id: ml + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.io/flavors/ + uses: oxsecurity/megalinter/flavors/java@v7.3.0 + env: + # All available variables are described in documentation + # https://megalinter.io/configuration/ + PRINT_ALPACA: false + # VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources + VALIDATE_ALL_CODEBASE: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY + DISABLE: COPYPASTE,MARKDOWN + DISABLE_LINTERS: SPELL_CSPELL,SPELL_LYCHEE,YAML_PRETTIER,REPOSITORY_GRYPE, REPOSITORY_SEMGREP,REPOSITORY_DEVSKIM,REPOSITORY_KICS,REPOSITORY_TRIVY,REPOSITORY_TRIVY_SBOM,REPOSITORY_TRUFFLEHOG,REPOSITORY_CHECKOV,REPOSITORY_GITLEAKS,YAML_V8R,JAVA_PMD,JAVA_CHECKSTYLE + YAML_YAMLLINT_CONFIG_FILE: ".yamllint.yaml" + OPENAPI_SPECTRAL_CONFIG_FILE: ".spectral.yml" + YAML_YAMLLINT_FILTER_REGEX_INCLUDE: "(code/)" + OPENAPI_SPECTRAL_FILTER_REGEX_INCLUDE: "(code/)" + + # Upload MegaLinter artifacts + - name: Archive production artifacts + if: ${{ success() }} || ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: MegaLinter reports + path: | + megalinter-reports + mega-linter.log \ No newline at end of file diff --git a/.github/workflows/spectral_oas_lint.yml b/.github/workflows/spectral_oas_lint.yml new file mode 100644 index 0000000..050bce6 --- /dev/null +++ b/.github/workflows/spectral_oas_lint.yml @@ -0,0 +1,36 @@ +--- +# CAMARA Project - workflow configuration to manually run CAMARA OAS rules +# see https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow +# 31.01.2024 - initial version + +name: Spectral manual run + +on: workflow_dispatch + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + build: + name: Spectral linting + runs-on: ubuntu-latest + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR + # Remove the ones you do not need + contents: write + issues: write + pull-requests: write + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances + - name: Install Spectral + run: npm install -g @stoplight/spectral + - name: Install Spectral functions + run: npm install -g @stoplight/spectral-functions + - name: Run Spectral linting + run: spectral lint code/API_definitions/*.yaml --verbose --ruleset .spectral.yml \ No newline at end of file diff --git a/.spectral.yml b/.spectral.yml new file mode 100644 index 0000000..ebaa5a5 --- /dev/null +++ b/.spectral.yml @@ -0,0 +1,261 @@ +# CAMARA Project - linting ruleset - documentation avaialable here: +# https://github.com/camaraproject/Commonalities/blob/main/documentation/Linting-rules.md +# Changelog: +# - 31.01.2024: Initial version +# - 19.03.2024: Corrected camara-http-methods rule + + +extends: "spectral:oas" +functions: + - camara-reserved-words + - camara-language-avoid-telco + - camara-security-no-secrets-in-path-or-query-parameters +functionsDir: "./lint_function" +rules: + # Built-in OpenAPI Specification ruleset. Each rule then can be enabled individually. + # The severity keyword is optional in rule definition and can be error, warn, info, hint, or off. The default value is warn. + contact-properties: false + duplicated-entry-in-enum: true + info-contact: true + info-description: true + info-license: true + license-url: true + no-$ref-siblings: error + no-eval-in-markdown: true + no-script-tags-in-markdown: true + openapi-tags: false + openapi-tags-alphabetical: false + openapi-tags-uniqueness: error + operation-description: true + operation-operationId: true + operation-operationId-unique: error + operation-operationId-valid-in-url: true + operation-parameters: true + operation-singular-tag: true + operation-success-response: true + operation-tags: true + operation-tag-defined: true + path-declarations-must-exist: true + path-keys-no-trailing-slash: true + path-not-include-query: true + path-params: error + tag-description: false + typed-enum: true + oas3-api-servers: true + oas3-examples-value-or-externalValue: true + oas3-operation-security-defined: false + oas3-parameter-description: false + oas3-schema: true + oas3-server-not-example.com: false + oas3-server-trailing-slash: true + oas3-unused-component: true + oas3-valid-media-example: true + oas3-valid-schema-example: true + # oas3-server-variables: true + + # Custom Rules Utilizing Spectral's Built-in Functions and JavaScript Implementations + + camara-language-avoid-telco: + message: "{{error}}" + severity: hint + description: | + This rule checks for telco-specific terminology in your API definitions and suggests more inclusive terms. + given: "$..*.*" + then: + function: camara-language-avoid-telco + recommended: false # Set to true/false to enable/disable this rule + + camara-oas-version: + message: "OpenAPI Version Error: The OpenAPI specification must adhere to version 3.0.3." + severity: error + description: | + This rule validates the OpenAPI version in your specification and requires compliance with version 3.0.3. + given: "$" + then: + field: openapi + function: pattern + functionOptions: + match: 3.0.3 + recommended: true # Set to true/false to enable/disable this rule + + camara-path-param-id: + message: "Path Parameter Naming Warning: Use 'resource_id' instead of just 'id' in path parameters." + severity: warn + description: | + This rule ensures consistent and descriptive naming for path parameters in your OpenAPI specification. + Please use 'resource_id' instead of just 'id' for your path parameters. + given: "$..parameters[?(@.in == 'path')]" + then: + field: name + function: pattern + functionOptions: + notMatch: \b(id|Id|ID|iD)\b + recommended: true # Set to true/false to enable/disable this rule + + camara-security-no-secrets-in-path-or-query-parameters: + message: "Sensitive data found in path: {{error}} Consider avoiding the use of Sesentive data " + severity: warn + description: | + This rule checks for sensitive data ('MSISDN' and 'IMSI') in API paths and suggests avoiding their use. + given: + - "$.paths" + then: + function: camara-security-no-secrets-in-path-or-query-parameters + recommended: true # Set to true/false to enable/disable this rule + + camara-http-methods: + description: "Ensure that all path URLs have valid HTTP methods (GET, PUT, POST, DELETE, PATCH, OPTIONS)." + message: "Invalid HTTP method for '{{path}}'. Must be one of get, put, post, delete, patch, options." + severity: error + given: $.paths[*][*]~ + then: + function: pattern + functionOptions: + match: "^(get|put|post|delete|patch|options|parameters)$" + recommended: true # Set to true/false to enable/disable this rule + + camara-get-no-request-body: + message: There must be no request body for Get and DELETE + severity: error + given: + - "$.paths.*.get" + - "$.paths.*.delete" + then: + field: requestBody + function: falsy + recommended: true # Set to true/false to enable/disable this rule + + camara-reserved-words: + message: "Reserved words found {{error}} Consider avoiding the use of reserved word " + severity: warn + description: | + This rule checks Reserved words must not be used in the following parts of an API specification [Paths, Request Body properties, Component, Operation Id, Security Schema] + given: + - "$.paths" # Paths + - "$..parameters[*]" # Path or Query Parameter Names: + - "$..components.schemas.*.properties.*" # Request and Response body parameter + - "$.paths.*." # Path and Operation Names: + - "$.components.securitySchemes" # Security Schemes: + - "$.components.*.*" # Component Names: + - "$.paths.*.*.operationId" # OperationIds: + then: + function: camara-reserved-words + recommended: true # Set to true/false to enable/disable this rule + + camara-routes-description: + message: "Functionality method description Warning: Each method should have description." + severity: warn + description: | + This rule checks if each operation (POST, GET, DELETE, PUT, PATCH, OPTIONS) in your API specification has a description. + Ensure that you have added a 'summary' field for each operation in your OpenAPI specification. + given: + - "$.paths.*.post" + - "$.paths.*.get" + - "$.paths.*.delete" + - "$.paths.*.put" + - "$.paths.*.patch" + - "$.paths.*.options" + then: + field: description + function: truthy + recommended: true # Set to true/false to enable/disable this rule + + camara-parameters-descriptions: + message: "Parameter description is missing or empty: {{error}}" + severity: warn + description: | + This Spectral rule ensures that each path parameter in the API specification has a descriptive and meaningful description. + given: + - "$.paths..parameters.*" + then: + field: description + function: truthy + recommended: true # Set to true/false to enable/disable this rule + + camara-response-descriptions: + message: "Parameter description is missing or empty: {{error}}" + severity: warn + description: | + This Spectral rule ensures that each responese object in the API specification has a descriptive and meaningful description. + given: + - "$.paths..responses.*" + then: + field: description + function: truthy + recommended: true # Set to true/false to enable/disable this rule + + camara-properties-descriptions: + message: "Property description is missing or empty: {{error}}" + severity: warn + description: | + This Spectral rule ensures that each propoerty within objects in the API specification has a descriptive and meaningful description. + given: + - "$.components.*.*" + - "$.components.*.*.properties.*" + then: + field: description + function: truthy + recommended: true # Set to true/false to enable/disable this rule + + camara-operation-summary: + message: "Operation Summary Warning: Each operation should include a short summary for better understanding." + severity: warn + description: | + This rule checks if each operation (POST, GET, DELETE, PUT, PATCH, OPTIONS) in your API specification has a meaningful summary. + Ensure that you have added a 'summary' field for each operation in your OpenAPI specification. + given: + - "$.paths.*.post" + - "$.paths.*.get" + - "$.paths.*.delete" + - "$.paths.*.put" + - "$.paths.*.patch" + - "$.paths.*.options" + then: + field: summary + function: truthy + recommended: true # Set to true/false to enable/disable this rule + + camara-discriminator-use: + description: | + Ensure that API definition YAML files with oneOf or anyOf sections include a discriminator object for serialization, deserialization, and validation. + severity: hint + given: "$..[?(@.oneOf || @.anyOf)]" + then: + field: discriminator + function: truthy + description: "Discriminator object is required when using oneOf or anyOf." + recommended: true # Set to true/false to enable/disable this rule + + camara-operationid-casing-convention: + message: Operation Id must be in Camel case "{{error}}" + severity: hint + description: | + This rule checks Operation ids should follow a specific case convention: camel case. + given: "$.paths.*.*.operationId" + then: + function: casing + functionOptions: + type: camel + recommended: true # Set to true/false to enable/disable this rule + + camara-schema-casing-convention: + description: This rule checks schema should follow a specific case convention pascal case. + message: "{{property}} should be pascal case (UppperCamelCase)" + severity: warn + given: $.components.schemas[*]~ + then: + function: casing + functionOptions: + type: pascal + recommended: true # Set to true/false to enable/disable this rule + + camara-parameter-casing-convention: + description: Paths should be kebab-case. + severity: error + message: "{{property}} is not kebab-case: {{error}}" + given: $.paths[*]~ + then: + function: pattern + functionOptions: + match: "^\/([a-z0-9]+(-[a-z0-9]+)*)?(\/[a-z0-9]+(-[a-z0-9]+)*|\/{.+})*$" # doesn't allow /asasd{asdas}sadas pattern or not closed braces + recommended: true # Set to true/false to enable/disable this rule \ No newline at end of file diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..f91fe9e --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,35 @@ +--- +# CAMARA Project - YAML linting configuration for yamllint https://yamllint.readthedocs.io/en/latest/rules.html +# 31.01.2024 - initial version + +yaml-files: + - '*.yaml' + - '*.yml' + - '.yamllint' + +rules: + braces: enable + brackets: enable + colons: enable + commas: enable + comments: + min-spaces-from-content: 1 + level: error + comments-indentation: + level: error + document-end: disable + document-start: disable + empty-lines: enable + empty-values: disable + hyphens: enable + indentation: enable + key-duplicates: enable + key-ordering: disable + line-length: disable + new-line-at-end-of-file: enable + new-lines: disable + octal-values: disable + quoted-strings: disable + trailing-spaces: enable + truthy: + level: error \ No newline at end of file diff --git a/code/API_definitions/SMS.yaml b/code/API_definitions/SMS.yaml index 8d04894..d3f7fd5 100644 --- a/code/API_definitions/SMS.yaml +++ b/code/API_definitions/SMS.yaml @@ -13,10 +13,38 @@ info: In order to the receive delivery receipt, separate API to be subscribed by the API consumer for receiving the delivery receipt in a standardized callback API. # Authorization and authentication - CAMARA guidelines defines a set of authorization flows which can grant API clients access to the API functionality, as outlined in the document [CAMARA-API-access-and-user-consent.md](https://github.com/camaraproject/IdentityAndConsentManagement/blob/main/documentation/CAMARA-API-access-and-user-consent.md). Which specific authorization flows are to be used will be determined during onboarding process, happening between the API Client and the Telco Operator exposing the API, taking into account the declared purpose for accessing the API, while also being subject to the prevailing legal framework dictated by local legislation. + + The "Camara Security and Interoperability Profile" provides details on how a client requests an access token. Please refer to Identify and Consent Management (https://github.com/camaraproject/IdentityAndConsentManagement/) for the released version of the Profile. + + Which specific authorization flows are to be used will be determined during onboarding process, happening between the API Client and the Telco Operator exposing the API, taking into account the declared purpose for accessing the API, while also being subject to the prevailing legal framework dictated by local legislation. It is important to remark that in cases where personal user data is processed by the API, and users can exercise their rights through mechanisms such as opt-in and/or opt-out, the use of 3-legged access tokens becomes mandatory. This measure ensures that the API remains in strict compliance with user privacy preferences and regulatory obligations, upholding the principles of transparency and user-centric data control. + # Identifying "from" i.e. sender identifer/number from the access token + + This specification defines the `from` field as optional in API requests, specifically in cases where the API is accessed using a 3-legged access token, and the sender identifer/number can be uniquely identified by the token. This approach simplifies API usage for API consumers by relying on the sender identifer/number information associated with the access token used to invoke the API. + + ## Handling of sender mobile number/identification information: + + ### Optional sender identifer/number for 3-legged tokens: + + - When using a 3-legged access token, the sender identifer/number associated with the access token must be considered as the "from" for the API request. This means that the sender identifer/number is not required in the request, and if included it must identify the same sender identifer/number, therefore **it is recommended NOT to include it in these scenarios** to simplify the API usage and avoid additional validations. + + ### Validation mechanism: + + - The server will extract the sender identifer/number from the access token, if available. + - If the API request additionally includes a `from` attribute when using a 3-legged access token, the API will validate that the sender identifer/number provided matches the one associated with the access token. + - If there is a mismatch, the API will respond with a 403 - INVALID_TOKEN_CONTEXT error, indicating that the sender identifer/number in the request does not match the token. + + ### Error handling for unidentifiable devices: + + - If the `from` attribute is not included in the request and the sender identifer/number information cannot be derived from the 3-legged access token, the server will return a 422 `UNIDENTIFIABLE_DEVICE` error. + + ### Restrictions for tokens without an associated authenticated identifier: + + - For scenarios which do not have a sender identifer/number associated to the token during the authentication flow, e.g. 2-legged access tokens, the `from` attribute MUST be provided in the API request. This ensures that the sender identifer/number is explicit and valid for each API call made with these tokens. + + version: 0.1.0-alpha.1 termsOfService: http://example.com/terms/ contact: @@ -48,7 +76,7 @@ paths: - openId: - send-sms:short-message tags: - - Send SMS + - Short Message Service summary: Send SMS description: | The customer application server makes a request to the SMS API to send SMS message to the destination address. @@ -88,6 +116,7 @@ paths: components: securitySchemes: openId: + description: OpenID Connect authentication type: openIdConnect openIdConnectUrl: https://example.com/.well-known/openid-configuration parameters: @@ -97,34 +126,38 @@ components: description: Correlation id for the different services schema: type: string - headers: - x-correlator: - description: Correlation id for the different services - schema: - type: string + # headers: + # x-correlator: + # description: Correlation id for the different services + # schema: + # type: string schemas: MessageRequest: + description: Request message object type: object required: - to - - from +# - from - message properties: to: type: array - description: "The recipients MSISDN" + description: "The recipients mobile number" items: - type: string + $ref: "#/components/schemas/PhoneNumber" + from: - type: string - description: "The senders MSISDN" + $ref: "#/components/schemas/PhoneNumber" + category: + description: Types of messages type: string enum: ["PROMOTION","SERVICE","TRANSACTION"] message: type: string description: "SMS message" MessageResponse: + description: Response message object type: object required: - msgId @@ -138,6 +171,7 @@ components: format: date-time description: Timestamp when SMS was delivered. It must follow [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6) and must have time zone. Recommended format is yyyy-MM-dd'T'HH:mm:ss.SSSZ (i.e. which allows 2023-07-03T14:27:08.312+02:00 or 2023-07-03T12:27:08.312Z) ErrorInfo: + description: Error message object type: object required: - status @@ -153,6 +187,12 @@ components: detail: type: string description: Detailed error description + + PhoneNumber: + description: A public identifier addressing a telephone subscription. In mobile networks it corresponds to the MSISDN (Mobile Station International Subscriber Directory Number). In order to be globally unique it has to be formatted in international format, according to E.164 standard, prefixed with '+'. + type: string + pattern: '^\+[1-9][0-9]{4,14}$' + example: "+123456789" responses: Generic200: description: OK @@ -265,4 +305,4 @@ components: example: status: 503 code: UNAVAILABLE - detail: Service unavailable + detail: Service unavailable \ No newline at end of file diff --git a/code/API_definitions/sms-delivery-notification-subscription.yaml b/code/API_definitions/sms-delivery-notification-subscription.yaml index f2f94b8..4d95736 100644 --- a/code/API_definitions/sms-delivery-notification-subscription.yaml +++ b/code/API_definitions/sms-delivery-notification-subscription.yaml @@ -100,7 +100,7 @@ paths: - SMS delivery notification subscription summary: 'Create a sms delivery event subscription for a given consumer' description: Create a sms delivery event subscription for a given consumer - operationId: createSMSDeliverySubscription + operationId: createSubscription parameters: - $ref: '#/components/parameters/x-correlator' security: @@ -114,7 +114,7 @@ paths: required: true callbacks: notifications: - "{$request.body#/webhook/notificationUrl}": + "{$request.body#/sink}": post: tags: - Session notifications callback @@ -189,7 +189,7 @@ paths: $ref: '#/components/responses/Generic503' get: tags: - - SMS Delivery notification subscription + - SMS delivery notification subscription summary: 'Retrieve a list of SMS Delivery event subscription' description: Retrieve a list of SMS Delivery event subscription(s) operationId: retrieveSubscriptionList @@ -197,7 +197,7 @@ paths: - $ref: '#/components/parameters/x-correlator' security: - openId: - - sim-swap:subscriptions:read + - send-sms:subscriptions:read responses: "200": description: List of event subscription details @@ -223,13 +223,13 @@ paths: /subscriptions/{subscriptionId}: get: tags: - - SMS Delivery notification subscription + - SMS delivery notification subscription summary: 'Retrieve a sms delivery event subscription for a phone number' description: retrieve event subscription information for a given subscription. operationId: retrieveSubscription security: - openId: - - sim-swap:subscriptions:read + - send-sms:subscriptions:read parameters: - $ref: "#/components/parameters/SubscriptionId" - $ref: '#/components/parameters/x-correlator' @@ -257,13 +257,13 @@ paths: $ref: "#/components/responses/Generic503" delete: tags: - - SMS Delivery notification subscription + - SMS delivery notification subscription summary: 'Delete a sms delivery event subscription' operationId: deleteSubscription description: delete a given event subscription. security: - openId: - - sim-swap:subscriptions:delete + - send-sms:subscriptions:delete parameters: - $ref: "#/components/parameters/SubscriptionId" - $ref: '#/components/parameters/x-correlator' @@ -297,6 +297,7 @@ paths: components: securitySchemes: openId: + description: OpenID Connect authentication type: openIdConnect openIdConnectUrl: https://example.com/.well-known/openid-configuration parameters: @@ -339,6 +340,7 @@ components: - FAIL ErrorInfo: + description: Error message object type: object required: - status @@ -356,41 +358,171 @@ components: description: Detailed error description CreateSubscription: - description: The request for creating a sim swap event subscription + description: The request for creating a event-type event subscription + type: object + required: + - sink + - protocol + - config + - types + properties: + protocol: + $ref: "#/components/schemas/Protocol" + sink: + type: string + format: url + description: The address to which events shall be delivered using the selected protocol. + example: "https://endpoint.example.com/sink" + sinkCredential: + description: A sink credential provides authentication or authorization information necessary to enable delivery of events to a target. + allOf: + - description: A sink credential provides authentication or authorization information necessary to enable delivery of events to a target. + - $ref: "#/components/schemas/SinkCredential" + types: + description: | + Camara Event types eligible to be delivered by this subscription. + Note: As of now we enforce to have only event type per subscription. + type: array + minItems: 1 + maxItems: 1 + items: + $ref: "#/components/schemas/SmsDeliveryNotificationEventType" + config: + $ref: "#/components/schemas/Config" + discriminator: + propertyName: protocol + mapping: + HTTP: "#/components/schemas/HTTPSubscriptionRequest" + MQTT3: "#/components/schemas/MQTTSubscriptionRequest" + MQTT5: "#/components/schemas/MQTTSubscriptionRequest" + AMQP: "#/components/schemas/AMQPSubscriptionRequest" + NATS: "#/components/schemas/NATSSubscriptionRequest" + KAFKA: "#/components/schemas/ApacheKafkaSubscriptionRequest" + + Protocol: + type: string + enum: ["HTTP", "MQTT3", "MQTT5", "AMQP", "NATS", "KAFKA"] + description: Identifier of a delivery protocol. Only HTTP is allowed for now + example: "HTTP" + + Config: + description: | + Implementation-specific configuration parameters needed by the subscription manager for acquiring events. + In CAMARA we have predefined attributes like `subscriptionExpireTime`, `subscriptionMaxEvents`, `initialEvent` + Specific event type attributes must be defined in `subscriptionDetail` + Note: if a request is performed for several event type, all subscribed event will use same `config` parameters. type: object required: - - webhook - subscriptionDetail properties: subscriptionDetail: - $ref: '#/components/schemas/SubscriptionDetail' + $ref: "#/components/schemas/SubscriptionDetail" subscriptionExpireTime: type: string format: date-time example: 2023-01-17T13:18:23.682Z - description: The subscription expiration time in date-time format. + description: The subscription expiration time (in date-time format) requested by the API consumer. subscriptionMaxEvents: type: integer - description: Identifies the maximum number of event reports to be generated (>=1) - Once this number is reached, the subscription ends. -1 means there is no limit. - example: -1 - webhook: - $ref: '#/components/schemas/Webhook' - - Webhook: - description: Webhook information for event channel + description: Identifies the maximum number of event reports to be generated (>=1) requested by the API consumer - Once this number is reached, the subscription ends. + minimum: 1 + example: 5 + initialEvent: + type: boolean + description: | + Set to `true` by API consumer if consumer wants to get an event as soon as the subscription is created and current situation reflects event request. + Example: Consumer request area entered event. If consumer sets initialEvent to true and device is already in the geofence, an event is triggered + + SinkCredential: + description: Sink credential type: object - required: - - notificationUrl properties: - notificationUrl: + credentialType: type: string - example: "https://application-server.com" - description: https callback address where the event notification must be POST-ed - notificationAuthToken: - type: string - example: "c8974e592c2fa383d4a3960714" - description: | - OAuth2 token to be used by the callback API endpoint. It MUST be indicated within HTTP Authorization header e.g. Authorization: Bearer $notificationAuthToken + enum: + - PLAIN + - ACCESSTOKEN + - REFRESHTOKEN + description: "The type of the credential." + discriminator: + propertyName: credentialType + mapping: + PLAIN: "#/components/schemas/PlainCredential" + ACCESSTOKEN: "#/components/schemas/AccessTokenCredential" + REFRESHTOKEN: "#/components/schemas/RefreshTokenCredential" + required: + - credentialType + PlainCredential: + type: object + description: A plain credential as a combination of an identifier and a secret. + allOf: + - $ref: "#/components/schemas/SinkCredential" + - type: object + required: + - identifier + - secret + properties: + identifier: + description: The identifier might be an account or username. + type: string + secret: + description: The secret might be a password or passphrase. + type: string + AccessTokenCredential: + type: object + description: An access token credential. + allOf: + - $ref: "#/components/schemas/SinkCredential" + - type: object + properties: + accessToken: + description: REQUIRED. An access token is a previously acquired token granting access to the target resource. + type: string + accessTokenExpiresUtc: + type: string + format: date-time + description: REQUIRED. An absolute UTC instant at which the token shall be considered expired. + accessTokenType: + description: REQUIRED. Type of the access token (See [OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-7.1)). + type: string + enum: + - bearer + required: + - accessToken + - accessTokenExpiresUtc + - accessTokenType + RefreshTokenCredential: + type: object + description: An access token credential with a refresh token. + allOf: + - $ref: "#/components/schemas/SinkCredential" + - type: object + properties: + accessToken: + description: REQUIRED. An access token is a previously acquired token granting access to the target resource. + type: string + accessTokenExpiresUtc: + type: string + format: date-time + description: REQUIRED. An absolute UTC instant at which the token shall be considered expired. + accessTokenType: + description: REQUIRED. Type of the access token (See [OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-7.1)). + type: string + enum: + - bearer + refreshToken: + description: REQUIRED. An refresh token credential used to acquire access tokens. + type: string + refreshTokenEndpoint: + type: string + format: uri + description: REQUIRED. A URL at which the refresh token can be traded for an access token. + required: + - accessToken + - accessTokenExpiresUtc + - accessTokenType + - refreshToken + - refreshTokenEndpoint SubscriptionDetail: type: object @@ -430,49 +562,83 @@ components: enum: - org.camaraproject.sms.v0.sms-delivery-status + - org.camaraproject.sms.v0.subscription-ends SubscriptionInfo: - description: Represents a sim swaps subscription. + description: Represents a event-type subscription. + type: object required: + - sink + - protocol + - config + - types + - id - startsAt - - subscriptionId - allOf: - - $ref: '#/components/schemas/CreateSubscription' - - type: object - properties: - subscriptionId: - $ref: '#/components/schemas/SubscriptionId' - subscriptionDetail: - $ref: '#/components/schemas/SubscriptionDetail' - subscriptionExpireTime: - type: string - format: date-time - example: 2023-01-17T13:18:23.682Z - description: The subscription expiration time in date-time format. - webhook: - $ref: '#/components/schemas/Webhook' - startsAt: - type: string - format: date-time - description: date time when subscription started - expiresAt: - type: string - format: date-time - description: date time when subscription will expire or expired - required: - - eventSubscriptionId - - type + properties: + protocol: + $ref: "#/components/schemas/Protocol" + sink: + type: string + format: url + description: The address to which events shall be delivered using the selected protocol. + example: "https://endpoint.example.com/sink" + sinkCredential: + $ref: "#/components/schemas/SinkCredential" + types: + description: | + Camara Event types eligible to be delivered by this subscription. + type: array + items: + type: string + config: + $ref: "#/components/schemas/Config" + id: + type: string + description: The unique identifier of the subscription in the scope of the subscription manager. When this information is contained within an event notification, this concept SHALL be referred as `subscriptionId` as per [Commonalities Event Notification Model](https://github.com/camaraproject/Commonalities/blob/main/documentation/API-design-guidelines.md#122-event-notification). + example: "1119920371" + startsAt: + type: string + format: date-time + description: Date when the event subscription will begin/began + expiresAt: + type: string + format: date-time + description: Date when the event subscription will expire. Only provided when `subscriptionExpireTime` is indicated by API client or Telco Operator has specific policy about that. + status: + type: string + description: |- + Current status of the subscription - Management of Subscription State engine is not mandatory for now. Note not all statuses may be considered to be implemented. Details: + - `ACTIVATION_REQUESTED`: Subscription creation (POST) is triggered but subscription creation process is not finished yet. + - `ACTIVE`: Subscription creation process is completed. Subscription is fully operative. + - `INACTIVE`: Subscription is temporarily inactive, but its workflow logic is not deleted. + - `EXPIRED`: Subscription is ended (no longer active). This status applies when subscription is ended due to `SUBSCRIPTION_EXPIRED` or `ACCESS_TOKEN_EXPIRED` event. + - `DELETED`: Subscription is ended as deleted (no longer active). This status applies when subscription information is kept (i.e. subscription workflow is no longer active but its meta-information is kept). + enum: + - ACTIVATION_REQUESTED + - ACTIVE + - EXPIRED + - INACTIVE + - DELETED + discriminator: + propertyName: protocol + mapping: + HTTP: "#/components/schemas/HTTPSubscriptionResponse" + MQTT3: "#/components/schemas/MQTTSubscriptionResponse" + MQTT5: "#/components/schemas/MQTTSubscriptionResponse" + AMQP: "#/components/schemas/AMQPSubscriptionResponse" + NATS: "#/components/schemas/NATSSubscriptionResponse" + KAFKA: "#/components/schemas/ApacheKafkaSubscriptionResponse" SubscriptionAsync: - description: Response for a sim swap operation managed asynchronously (Creation or Deletion) + description: Response for a event-type subscription request managed asynchronously (Creation or Deletion) type: object properties: - subscriptionId: - $ref: '#/components/schemas/SubscriptionId' + id: + $ref: "#/components/schemas/SubscriptionId" SubscriptionId: type: string - description: The event subscription identifier. + description: The unique identifier of the subscription in the scope of the subscription manager. When this information is contained within an event notification, this concept SHALL be referred as `subscriptionId` as per [Commonalities Event Notification Model](https://github.com/camaraproject/Commonalities/blob/main/documentation/API-design-guidelines.md#122-event-notification). example: qs15-h556-rt89-1298 CloudEvent: @@ -494,7 +660,7 @@ components: specversion: type: string description: Version of the specification to which this event conforms (must be 1.0 if it conforms to cloudevents 1.0.2 version) - example: 1.0 + example: '1.0' datacontenttype: type: string description: 'media-type that describes the event payload encoding, must be "application/json" for CAMARA APIs' @@ -514,14 +680,18 @@ components: type: string format: uri-reference minLength: 1 - description: "Identifies the context in which an event happened in the specific Provider Implementation." - example: - - https://github.com/cloudevents - - mailto:cncf-wg-serverless@lists.cncf.io - - urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66 - - cloudevents/spec/pull/123 - - /sensors/tn-1234567/alerts - - 1-555-123-4567 + description: | + Identifies the context in which an event happened - be a non-empty `URI-reference` like: + - URI with a DNS authority: + * https://github.com/cloudevents + * mailto:cncf-wg-serverless@lists.cncf.io + - Universally-unique URN with a UUID: + * urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66 + - Application-specific identifier: + * /cloudevents/spec/pull/123 + * 1-555-123-4567 + example: "https://notificationSendServer12.supertelco.com" + DateTime: type: string @@ -529,26 +699,6 @@ components: description: Timestamp of when the occurrence happened. Must adhere to RFC 3339. example: '2018-04-05T17:31:00Z' - EventSimSwap: - description: event structure for swapped event - allOf: - - $ref: '#/components/schemas/CloudEvent' - - type: object - properties: - data: - $ref: '#/components/schemas/DeliveryStatus' - - EventSimSwapSubscriptionEnds: - description: event structure for event subscription ends - allOf: - - $ref: '#/components/schemas/CloudEvent' - - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/SubscriptionEnds' - DeliveryStatus: description: Event detail structure for SMS Delivery event. type : object @@ -578,6 +728,7 @@ components: terminationReason: $ref: "#/components/schemas/TerminationReason" terminationDescription: + description: termination description type: string TerminationReason: @@ -709,8 +860,9 @@ components: message: "Expected property is missing: subscriptionId" examples: STATUS: + description: Example of Status value: - id: 123655 + id: '123655' source: supertelco.notificationSendServer12 type: org.camaraproject.sms.v0.sms-delivery-status specversion: "1.0" @@ -721,10 +873,11 @@ components: time: 2023-01-18T13:18:23.682Z SUBSCRIPTION_ENDS: + description: example for subscription ends value: - id: 123658 + id: '123658' source: supertelco.notificationSendServer12 - type: org.camaraproject.sim-swap.v0.subscription-ends + type: org.camaraproject.sms.v0.subscription-ends specversion: "1.0" datacontenttype: application/json data: diff --git a/code/Test_definitions/send-sms.feature b/code/Test_definitions/send-sms.feature new file mode 100644 index 0000000..4c08cd3 --- /dev/null +++ b/code/Test_definitions/send-sms.feature @@ -0,0 +1,131 @@ +Feature: CAMARA ShortMessageService API, Operation sendSMS + # Input to be provided by the implementation to the tester + # + # + # Testing assets: + # * A mobile number/Identifier (aka Short Code) from which SMS is to be sent. + # * A mobile number to which SMS is to be sent. + # + # References to OAS spec schemas refer to schemas specifies in SMS.yaml + + Background: Common SMS setup + Given the resource "/sms/v0rc1" | + And the header "Content-Type" is set to "application/json" + And the header "Authorization" is set to a valid access token + And the header "x-correlator" is set to a UUID value + And the request body is set by default to a request body compliant with the schema + + # Happy path scenarios + + # This first scenario serves as a minimum success scenario + @send_sms_01_generic_success_scenario + Scenario: Common validations for any success scenario + # Valid testing mobile number and default request body compliant with the schema + Given the request body property "$.to" is set to a valid testing mobile number + And the request body property "$.from" is set to a valid testing mobile number + And the request body property "$.category" is set to a valid category value + When the HTTP "POST" request is sent + Then the response status code is 200 + And the response header "Content-Type" is "application/json" + And the response header "x-correlator" has same value as the request header "x-correlator" + # The response has to comply with the generic response schema which is part of the spec + And the response body complies with the OAS schema at "/components/schemas/Location" + + + # Error scenarios for phone number + + @location_retrieval_10_empty_receiver + Scenario: The 'to' attribute value is empty + Given the request body property "$.to" is set to empty object + When the HTTP "POST" request is sent + Then the response status code is 400 + And the response property "$.status" is 400 + And the response property "$.code" is "INVALID_ARGUMENT" + And the response property "$.message" contains a user friendly text + + @location_retrieval_11_schema_compliant + # Test every type of identifier even if not supported by the implementation + Scenario Outline: 'to' attribute value does not comply with the schema + Given the request body property "$.to" does not comply with the OAS schema + When the HTTP "POST" request is sent + Then the response status code is 400 + And the response property "$.status" is 400 + And the response property "$.code" is "INVALID_ARGUMENT" + And the response property "$.message" contains a user friendly text + + @location_retrieval_11_schema_compliant + # Test every type of identifier even if not supported by the implementation + Scenario Outline: 'from' attribute value does not comply with the schema + Given the request body property "$.from" does not comply with the OAS schema + When the HTTP "POST" request is sent + Then the response status code is 400 + And the response property "$.status" is 400 + And the response property "$.code" is "INVALID_ARGUMENT" + And the response property "$.message" contains a user friendly text + + + Examples: + | phone_number_value | + | string_value | + | 1234567890 | + | +12334foo22222 | + | +00012230304913849 | + | 123 | + | ++49565456787 | + + + # Generic 400 errors + + @location_retrieval_400.1_no_request_body + Scenario: Missing request body + Given the request body is not included + When the HTTP "POST" request is sent + Then the response status code is 400 + And the response property "$.status" is 400 + And the response property "$.code" is "INVALID_ARGUMENT" + And the response property "$.message" contains a user friendly text + + @location_retrieval_400.2_empty_request_body + Scenario: Empty object as request body + Given the request body is set to "{}" + When the HTTP "POST" request is sent + Then the response status code is 400 + And the response property "$.status" is 400 + And the response property "$.code" is "INVALID_ARGUMENT" + And the response property "$.message" contains a user friendly text + + + # Generic 401 errors + + @location_retrieval_401.1_no_authorization_header + Scenario: No Authorization header + Given the header "Authorization" is removed + And the request body is set to a valid request body + When the HTTP "POST" request is sent + Then the response status code is 401 + And the response property "$.status" is 401 + And the response property "$.code" is "UNAUTHENTICATED" + And the response property "$.message" contains a user friendly text + + + @location_retrieval_401.2_expired_access_token + Scenario: Expired access token + Given the header "Authorization" is set to an expired access token + And the request body is set to a valid request body + When the HTTP "POST" request is sent + Then the response status code is 401 + And the response property "$.status" is 401 + And the response property "$.code" is "UNAUTHENTICATED" + And the response property "$.message" contains a user friendly text + + + @location_retrieval_401.3_invalid_access_token + Scenario: Invalid access token + Given the header "Authorization" is set to an invalid access token + And the request body is set to a valid request body + When the HTTP "POST" request is sent + Then the response status code is 401 + And the response header "Content-Type" is "application/json" + And the response property "$.status" is 401 + And the response property "$.code" is "UNAUTHENTICATED" + And the response property "$.message" contains a user friendly text \ No newline at end of file diff --git a/lint-function/camara-language-avoid-telco.js b/lint-function/camara-language-avoid-telco.js new file mode 100644 index 0000000..18d4052 --- /dev/null +++ b/lint-function/camara-language-avoid-telco.js @@ -0,0 +1,40 @@ +// CAMARA Project - support function for Spectral linter +// 31.01.2024 - initial version + +const replacements = [ + { original: 'UE', recommended: 'device' }, + { original: 'MSISDN', recommended: 'phone number' }, + { original: 'mobile network', recommended: 'network' } + ]; + + export default async function (input) { + const errors = []; + const suggestions = []; + + // Iterate over properties of the input object + for (const path in input) { + const value = input[path]; + + // Check if the value is a string + if (typeof value === 'string') { + for (const replacement of replacements) { + const original = replacement.original; + const recommended = replacement.recommended; + + // Use a regular expression to match 'original' as a standalone word + const regex = new RegExp(`\\b${original}\\b`, 'g'); + + // Check if 'original' exists in the value + if (regex.test(value)) { + errors.push(replacement); + suggestions.push(` Telco-specific terminology found in input: Consider replacing '${original}' with '${recommended}'.`); + } + } + } + } + + // Check if any word from 'replacements' is in the suggestions + if (errors.length > 0) { + console.log(`Hint camara-language-avoid-telco ` + suggestions.join(', ')); + } + }; \ No newline at end of file diff --git a/lint-function/camara-reserved-words.js b/lint-function/camara-reserved-words.js new file mode 100644 index 0000000..69a2a47 --- /dev/null +++ b/lint-function/camara-reserved-words.js @@ -0,0 +1,98 @@ +// CAMARA Project - support function for Spectral linter +// 31.01.2024 - initial version + +const reservedWords = [ + 'abstract', + 'apiclient', + 'apiexception', + 'apiresponse', + 'assert', + 'boolean', + 'break', + 'byte', + 'case', + 'catch', + 'char', + 'class', + 'configuration', + 'const', + 'continue', + 'do', + 'double', + 'else', + 'extends', + 'file', + 'final', + 'finally', + 'float', + 'for', + 'goto', + 'if', + 'implements', + 'import', + 'instanceof', + 'int', + 'interface', + 'list', + 'localdate', + 'localreturntype', + 'localtime', + 'localvaraccept', + 'localvaraccepts', + 'localvarauthnames', + 'localvarcollectionqueryparams', + 'localvarcontenttype', + 'localvarcontenttypes', + 'localvarcookieparams', + 'localvarformparams', + 'localvarheaderparams', + 'localvarpath', + 'localvarpostbody', + 'localvarqueryparams', + 'long', + 'native', + 'new', + 'null', + 'object', + 'offsetdatetime', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'short', + 'static', + 'strictfp', + 'stringutil', + 'super', + 'switch', + 'synchronized', + 'this', + 'throw', + 'throws', + 'transient', + 'try', + 'void', + 'volatile', + 'while' + ]; + // Reserved word 'enum' and 'default' are removed from above reserved word array as they are common in openAPI keyword + export default async function lintReservedWords(input) { + // Iterate over properties of the input object + for (const path in input) { + if (typeof path === 'string') { + + for (const word of reservedWords) { + const regex = new RegExp(`\\b${word}\\b`, 'g'); // Use a regular expression to match 'word' as a standalone word + + if (regex.test(path)) { + const warningRuleName = 'camara-reserved-words'; + const description = `Reserved words found in input: Consider avoiding the use of reserved word '${word}'`; + // const location = `${path}`; + + console.log(`warning ${warningRuleName} ${description} ${path}`); + } + } + } + } + } \ No newline at end of file diff --git a/lint-function/camara-security-no-secrets-in-path-or-query-parameters.js b/lint-function/camara-security-no-secrets-in-path-or-query-parameters.js new file mode 100644 index 0000000..0bda425 --- /dev/null +++ b/lint-function/camara-security-no-secrets-in-path-or-query-parameters.js @@ -0,0 +1,26 @@ +// CAMARA Project - support function for Spectral linter +// 31.01.2024 - initial version + +const sensitiveData = ['MSISDN','IMSI','phoneNumber']; + +export default async function (input) { + + // Iterate over properties of the input object + for (const path in input) { + + if (typeof path === 'string') { + for (const word of sensitiveData ) { + const regex = new RegExp(`\\b${word}\\b`, 'g'); // Use a regular expression to match 'word' as a standalone word + + if (regex.test(path)) { + + const warningRuleName = 'camara-security-no-secrets-in-path-or-query-parameters'; + const description = `sensitiveData Data found in path: Consider avoiding the use of sensitiveData data '${word}'`; + const location = `paths.${path}`; + console.log(`warning ${warningRuleName} ${description} ${location}`); + + } + } + } + } +} \ No newline at end of file