openapi: 3.1.0
info:
  title: SoS Admin - Users & Groups API
  description: |
    API for managing users, groups, and group user assignments in the SoS Admin platform.
    - **All callers**: authenticate with a bearer token issued by the OIDC provider (see `oauth2_oidc` security scheme).s
    - Collection responses use offset-based pagination (`offset`, `limit`, `total`) with a top-level `_links` envelope containing plain URI strings.
    - Single resource responses include HATEOAS `_links` pointing to self, the parent collection, and key related resources.
  version: 1.0.0
  contact:
    email: lpowell@gradera.ai

servers:
  - url: https://dev-apis.gradera.ai/sos-admin/v1
    description: Development server
  - url: https://qa-apis.gradera.ai/sos-admin/v1
    description: Quality Assurance server
  - url: https://stage-apis.gradera.ai/sos-admin/v1
    description: Staging server
  - url: https://cte-apis.gradera.ai/sos-admin/v1
    description: Consumer Test Environment server
  - url: https://apis.gradera.ai/sos-admin/v1
    description: Production server

tags:
  - name: Users
    description: Operations for creating, retrieving, replacing, and deleting users.
  - name: Groups
    description: Operations for creating, retrieving, replacing, and deleting groups.
  - name: Group User Assignments
    description: Operations for creating, retrieving, and deleting user-to-group membership assignments.

# ============================================================
# PATHS
# ============================================================
paths:

  # ----------------------------------------------------------
  # Users
  # ----------------------------------------------------------
  /users:
    get:
      tags:
        - Users
      summary: List users
      operationId: listUsers
      description: |
        Returns a paginated list of users.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Paginated list of users
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsersPage'
              examples:
                users-page:
                  $ref: '#/components/examples/UsersPageExample'
        '400':
          description: Invalid request parameters for listing users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Errors'
              examples:
                users-list-badrequest:
                  $ref: '#/components/examples/UsersListBadRequestExample'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    post:
      tags:
        - Users
      summary: Create a user
      operationId: createUser
      description: |
        Creates a new user. The server generates and assigns the `userId`.
        - On success, returns `201 Created` with a `Location` header pointing to the new resource.
        - Returns `409 Conflict` if a user with the same `email` already exists.
      security:
        - oauth2_oidc: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInput'
            examples:
              create-user:
                $ref: '#/components/examples/UserInputExample'
      responses:
        '201':
          description: User created successfully
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
            Location:
              $ref: '#/components/headers/Location'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                created-user:
                  $ref: '#/components/examples/UserCreatedExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  /users/{userId}:
    get:
      tags:
        - Users
      summary: Get a user by ID
      operationId: getUser
      description: Retrieves the full user record by `userId`.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/userId'
      responses:
        '200':
          description: User details
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                user:
                  $ref: '#/components/examples/UserExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    put:
      tags:
        - Users
      summary: Replace a user
      operationId: replaceUser
      description: |
        Fully replaces the user identified by `userId`. All mutable fields must be provided.
        The `userId` is immutable and taken from the path; supplying it in the body is not permitted.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/userId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInput'
            examples:
              replace-user:
                $ref: '#/components/examples/UserInputExample'
      responses:
        '200':
          description: User replaced successfully
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                user:
                  $ref: '#/components/examples/UserExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    delete:
      tags:
        - Users
      summary: Delete a user
      operationId: deleteUser
      description: |
        Permanently deletes the user identified by `userId`.
        - Returns `204 No Content` on success.
        - Callers should remove any group assignments for this user prior to deletion, or expect cascading behavior per environment policy.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/userId'
      responses:
        '204':
          description: User deleted successfully. No content returned.
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  # ----------------------------------------------------------
  # Groups
  # ----------------------------------------------------------
  /groups:
    get:
      tags:
        - Groups
      summary: List groups
      operationId: listGroups
      description: Returns a paginated list of groups.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Paginated list of groups
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupsPage'
              examples:
                groups-page:
                  $ref: '#/components/examples/GroupsPageExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    post:
      tags:
        - Groups
      summary: Create a group
      operationId: createGroup
      description: |
        Creates a new group.
        - On success, returns `201 Created` with a `Location` header pointing to the new resource.
        - Returns `409 Conflict` if a group with the same `groupName` already exists.
      security:
        - oauth2_oidc: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupInput'
            examples:
              create-group:
                $ref: '#/components/examples/GroupInputExample'
      responses:
        '201':
          description: Group created successfully
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
            Location:
              $ref: '#/components/headers/Location'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
              examples:
                group:
                  $ref: '#/components/examples/GroupExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  /groups/{groupSlug}:
    get:
      tags:
        - Groups
      summary: Get a group by slug
      operationId: getGroup
      description: Retrieves the full group record by `groupSlug`.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/groupSlug'
      responses:
        '200':
          description: Group details
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
              examples:
                group:
                  $ref: '#/components/examples/GroupExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    put:
      tags:
        - Groups
      summary: Replace a group
      operationId: replaceGroup
      description: |
        Fully replaces the group identified by `groupSlug`. All mutable fields must be provided.
        The `groupSlug` is the canonical resource identifier taken from the path; supplying it in the body is not permitted.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/groupSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupInput'
            examples:
              replace-group:
                $ref: '#/components/examples/GroupInputExample'
      responses:
        '200':
          description: Group replaced successfully
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
              examples:
                group:
                  $ref: '#/components/examples/GroupExample'

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    delete:
      tags:
        - Groups
      summary: Delete a group
      operationId: deleteGroup
      description: |
        Permanently deletes the group identified by `groupSlug`.
        - Returns `204 No Content` on success.
        - Callers should remove all group assignments for this group prior to deletion, or expect cascading behavior per environment policy.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/groupSlug'
      responses:
        '204':
          description: Group deleted successfully. No content returned.
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Resource conflict (group has users assigned)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Errors'
              examples:
                group-delete-conflict:
                  $ref: '#/components/examples/ConflictExample'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  # ----------------------------------------------------------
  # Group User Assignments
  # ----------------------------------------------------------
  /group-user-assignments:
    get:
      tags:
        - Group User Assignments
      summary: List group user assignments
      operationId: listGroupUserAssignments
      description: |
        Returns a paginated list of group membership assignments.
        Use `groupSlug` to list all members of a group, or `userId` to list all groups a user belongs to.
        Both filters may be combined to check a specific membership.
      security:
        - oauth2_oidc: []
      parameters:
        - name: groupSlug
          in: query
          required: false
          description: Filter assignments by group slug.
          schema:
            type: string
            minLength: 1
            maxLength: 50
          example: product-managers
        - name: userId
          in: query
          required: false
          description: Filter assignments by user UUID.
          schema:
            type: string
            format: uuid
          example: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Paginated list of group assignments
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupAssignmentsPage'
              examples:
                assignments-page:
                  $ref: '#/components/examples/GroupAssignmentsPageExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    post:
      tags:
        - Group User Assignments
      summary: Create a group user assignment
      operationId: createGroupUserAssignment
      description: |
        Assigns a user to a group. The server generates and assigns the `groupAssignId`.
        - On success, returns `201 Created` with a `Location` header.
        - Returns `409 Conflict` if the user is already a member of the specified group.
      security:
        - oauth2_oidc: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupAssignmentInput'
            examples:
              create-assignment:
                $ref: '#/components/examples/GroupAssignmentInputExample'
      responses:
        '201':
          description: Group assignment created successfully
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
            Location:
              $ref: '#/components/headers/Location'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupAssignment'
              examples:
                created-assignment:
                  $ref: '#/components/examples/GroupAssignmentExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  /group-user-assignments/{groupUserAssignId}:
    get:
      tags:
        - Group User Assignments
      summary: Get a group user assignment by ID
      operationId: getGroupUserAssignment
      description: Retrieves a single group user assignment record by `groupUserAssignId`.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/groupUserAssignId'
      responses:
        '200':
          description: Group assignment details
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupAssignment'
              examples:
                assignment:
                  $ref: '#/components/examples/GroupAssignmentExample'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    delete:
      tags:
        - Group User Assignments
      summary: Delete a group assignment
      operationId: deleteGroupAssignment
      description: |
        Removes the user-to-group membership identified by `groupAssignId`.
        Returns `204 No Content` on success.
      security:
        - oauth2_oidc: []
      parameters:
        - $ref: '#/components/parameters/groupUserAssignId'
      responses:
        '204':
          description: Group assignment deleted successfully. No content returned.
          headers:
            API-Version:
              $ref: '#/components/headers/API-Version'
            Correlation-Key:
              $ref: '#/components/headers/Correlation-Key'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RequestLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServiceError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

# ============================================================
# COMPONENTS
# ============================================================
components:

  # ----------------------------------------------------------
  # Schemas
  # ----------------------------------------------------------
  schemas:

    # === Shared: HATEOAS link object ===
    Link:
      type: object
      description: A single HATEOAS hypermedia link.
      required:
        - href
      additionalProperties: false
      properties:
        href:
          type: string
          format: uri
          example: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366

    # === Shared: Pagination links ===
    PaginationLinks:
      type: object
      description: HATEOAS navigation links for a paginated collection response (plain URI strings).
      required:
        - self
        - first
        - last
      additionalProperties: false
      properties:
        self:
          type: string
          format: uri
          description: URL of the current page.
        first:
          type: string
          format: uri
          description: URL of the first page.
        last:
          type: string
          format: uri
          description: URL of the last page.
        next:
          type: [string, 'null']
          format: uri
          description: URL of the next page; `null` when on the last page.
        prev:
          type: [string, 'null']
          format: uri
          description: URL of the previous page; `null` when on the first page.

    # === User links ===
    UserLinks:
      type: object
      description: HATEOAS links included on a User resource response.
      required:
        - self
        - collection
        - assignments
      additionalProperties: false
      properties:
        self:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: Canonical URL of this user.
        collection:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL of the users collection.
        assignments:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL to retrieve all group assignments for this user.

    # === Group links ===
    GroupLinks:
      type: object
      description: HATEOAS links included on a Group resource response.
      required:
        - self
        - collection
        - assignments
      additionalProperties: false
      properties:
        self:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: Canonical URL of this group.
        collection:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL of the groups collection.
        assignments:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL to retrieve all group assignments for this group.

    # === GroupAssignment links ===
    GroupAssignmentLinks:
      type: object
      description: HATEOAS links included on a GroupAssignment resource response.
      required:
        - self
        - collection
        - user
        - group
      additionalProperties: false
      properties:
        self:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: Canonical URL of this group assignment.
        collection:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL of the group assignments collection.
        user:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL of the assigned user.
        group:
          allOf:
            - $ref: '#/components/schemas/Link'
          description: URL of the target group.

    # === INPUT: Create or Replace User ===
    UserInput:
      type: object
      description: Input for creating or fully replacing a user. The `userId` is server-managed and must not be included.
      required:
        - userName
        - email
      additionalProperties: false
      properties:
        userName:
          type: string
          minLength: 1
          maxLength: 100
          description: Full display name of the user.
          example: Lou Powell
        email:
          type: string
          format: email
          description: Unique email address for the user.
          example: lpowell@gradera.ai

    # === OUTPUT: User ===
    User:
      type: object
      description: A user resource.
      required:
        - userId
        - userName
        - email
        - _links
      additionalProperties: false
      properties:
        userId:
          type: string
          format: uuid
          readOnly: true
          description: Server-generated unique identifier for this user.
          example: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
        cname: 
          type: string  
          format: uri
          description: Canonical name (URI) of the user, derived from the `userId`. This is a read-only field provided for convenience and consistency in resource identification.
          example: niq://acme-corp/users/jdoe@email.com
          minLength: 1
          maxLength: 255
        userName:
          type: string
          description: Full display name of the user.
          minLength: 1
          maxLength: 100
          example: John Doe
        email:
          type: string
          format: email
          description: Unique email address for the user.
          example: jdoe@email.com
        _links:
          $ref: '#/components/schemas/UserLinks'

    # === OUTPUT: Users paginated page ===
    UsersPage:
      type: object
      description: A paginated collection of users.
      required:
        - offset
        - limit
        - total
        - _links
        - data
      additionalProperties: false
      properties:
        offset:
          type: integer
          minimum: 0
          description: Zero-based record offset for this page.
        limit:
          type: integer
          minimum: 1
          maximum: 1000
          description: Maximum number of items returned.
        total:
          type: integer
          minimum: 0
          description: Total number of items matching the query.
        _links:
          $ref: '#/components/schemas/PaginationLinks'
        data:
          type: array
          items:
            $ref: '#/components/schemas/User'

    # === INPUT: Create or Replace Group ===
    GroupInput:
      type: object
      description: Input for creating or fully replacing a group.
      required:
        - groupName
        - groupSlug
        - groupDesc
      additionalProperties: false
      properties:
        groupName:
          type: string
          minLength: 1
          maxLength: 50
          description: Human-readable display name for the group.
          example: Product Managers
        groupSlug: 
          type: string
          description: Group slug for use in the cname and for readable identification. 
          example: product-managers
          minLength: 1
          maxLength: 50
        groupDesc:
          type: string
          minLength: 1
          description: Description of the group and its purpose.
          example: >-
            Responsible for defining the product vision, strategy, and roadmap.
            Collaborates with cross-functional teams to deliver products that meet
            market needs and business goals.

    # === OUTPUT: Group ===
    Group:
      type: object
      description: A group resource.
      required:
        - groupSlug
        - groupName
        - groupDesc
        - _links
        - created_dts
        - updated_dts
      additionalProperties: false
      properties:
        groupName:
          type: string
          description: Human-readable display name for the group.
          example: Product Managers
        groupSlug: 
          type: string
          description: Group slug for use in the cname and for readable identification. 
          example: product-managers
          minLength: 1
          maxLength: 50
        cname: 
          type: string  
          format: uri
          description: Canonical name (URI) of the group, derived from the group's slug. This is a read-only field provided for convenience and consistency in resource identification.
          example: niq://acme-corp/groups/product-managers
          minLength: 1
          maxLength: 255
        groupDesc:
          type: string
          description: Description of the group and its purpose.
          example: >-
            Responsible for defining the product vision, strategy, and roadmap.
            Collaborates with cross-functional teams to deliver products that meet
            market needs and business goals.
        created_dts:
          type: string
          format: date-time
          readOnly: true
          description: Timestamp (UTC) when the group was created.
          example: '2026-01-15T09:00:00Z'
        updated_dts:
          type: string
          format: date-time
          readOnly: true
          description: Timestamp (UTC) when the group was last updated.
          example: '2026-01-15T09:00:00Z'
        _links:
          $ref: '#/components/schemas/GroupLinks'

    # === OUTPUT: Groups paginated page ===
    GroupsPage:
      type: object
      description: A paginated collection of groups.
      required:
        - offset
        - limit
        - total
        - _links
        - data
      additionalProperties: false
      properties:
        offset:
          type: integer
          minimum: 0
          description: Zero-based record offset for this page.
        limit:
          type: integer
          minimum: 1
          maximum: 1000
          description: Maximum number of items returned.
        total:
          type: integer
          minimum: 0
          description: Total number of items matching the query.
        _links:
          $ref: '#/components/schemas/PaginationLinks'
        data:
          type: array
          items:
            $ref: '#/components/schemas/Group'

    # === INPUT: Create Group Assignment ===
    GroupAssignmentInput:
      type: object
      description: Input for creating a group assignment. The `groupAssignId` is server-managed and must not be included.
      required:
        # Require the target group slug and the user being assigned
        - groupSlug
        - userId
      additionalProperties: false
      properties:
        groupSlug:
          type: string
          description: Slug identifier of the target group to which the user will be assigned.
          example: product-managers
          minLength: 1
          maxLength: 50
        userId:
          type: string
          format: uuid
          description: UUID of the user being assigned to the group.
          example: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366

    # === OUTPUT: GroupAssignment ===
    GroupAssignment:
      type: object
      description: A group membership assignment resource.
      required:
        - groupUserAssignId
        - groupSlug
        - userId
        - _links
      additionalProperties: false
      properties:
        groupUserAssignId:
          type: string
          format: uuid
          readOnly: true
          description: Server-generated unique identifier for this assignment.
          example: 8aacd616-0c81-4935-90d4-e00b677bd200
        groupSlug:
          type: string
          description: Slug identifier of the group this assignment belongs to.
          example: product-managers
          minLength: 1
          maxLength: 50
        userId:
          type: string
          format: uuid
          description: UUID of the user.
          example: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
        created_dts:
          type: string
          format: date-time
          readOnly: true
          description: Timestamp (UTC) when the assignment was created.
          example: '2026-02-18T12:34:56Z'
        _links:
          $ref: '#/components/schemas/GroupAssignmentLinks'

    # === OUTPUT: GroupAssignments paginated page ===
    GroupAssignmentsPage:
      type: object
      description: A paginated collection of group assignments.
      required:
        - offset
        - limit
        - total
        - _links
        - data
      additionalProperties: false
      properties:
        offset:
          type: integer
          minimum: 0
          description: Zero-based record offset for this page.
        limit:
          type: integer
          minimum: 1
          maximum: 1000
          description: Maximum number of items returned.
        total:
          type: integer
          minimum: 0
          description: Total number of items matching the query.
        _links:
          $ref: '#/components/schemas/PaginationLinks'
        data:
          type: array
          items:
            $ref: '#/components/schemas/GroupAssignment'

    # === Error ===
    Error:
      type: object
      required:
        - code
        - title
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this specific error occurrence; useful for support troubleshooting.
          example: 1d2a3e4f-5b6c-7d8e-9f0a-1b2c3d4e5f6a
        code:
          type: string
          description: Application-specific error code.
          example: ADM400
        title:
          type: string
          description: Short, human-readable summary of the problem.
          example: Invalid request parameters
        detail:
          type: string
          description: Human-readable explanation specific to this occurrence of the problem.
          example: The `email` field must be a valid email address.

    Errors:
      type: array
      items:
        $ref: '#/components/schemas/Error'

  # ----------------------------------------------------------
  # Responses
  # ----------------------------------------------------------
  responses:
    BadRequest:
      description: Invalid request parameters or body
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            badRequest:
              $ref: '#/components/examples/BadRequestExample'

    Unauthorized:
      description: Missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            unauthorized:
              $ref: '#/components/examples/UnauthorizedExample'

    Forbidden:
      description: Insufficient permissions to perform this operation
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            forbidden:
              $ref: '#/components/examples/ForbiddenExample'

    NotFound:
      description: The requested resource does not exist
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            notFound:
              $ref: '#/components/examples/NotFoundExample'

    Conflict:
      description: Resource already exists or violates a uniqueness constraint
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            conflict:
              $ref: '#/components/examples/ConflictExample'

    UnsupportedMediaType:
      description: Unsupported media type; use `application/json`
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            unsupportedMediaType:
              $ref: '#/components/examples/UnsupportedMediaTypeExample'

    RequestLimitExceeded:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            rateLimit:
              $ref: '#/components/examples/RequestLimitExceededExample'

    InternalServiceError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            internalError:
              $ref: '#/components/examples/InternalServiceErrorExample'

    ServiceUnavailable:
      description: Service is temporarily unavailable
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Errors'
          examples:
            unavailable:
              $ref: '#/components/examples/ServiceUnavailableExample'

  # ----------------------------------------------------------
  # Headers
  # ----------------------------------------------------------
  headers:
    API-Version:
      description: API version that handled this request
      required: true
      schema:
        type: string
        example: 1.0.0
    Correlation-Key:
      description: Unique request trace identifier for diagnostics
      required: true
      schema:
        type: string
        format: uuid
        example: a1b2c3d4-e5f6-7a8b-9c0d-e1f2a3b4c5d6
    Location:
      description: URI of the newly created resource
      required: true
      schema:
        type: string
        format: uri
        example: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366

  # ----------------------------------------------------------
  # Parameters
  # ----------------------------------------------------------
  parameters:
    userId:
      name: userId
      in: path
      required: true
      description: UUID of the user.
      schema:
        type: string
        format: uuid
      example: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366

    groupSlug:
      name: groupSlug
      in: path
      required: true
      description: Slug identifier of the group used as the canonical resource identifier in paths.
      schema:
        type: string
        minLength: 1
        maxLength: 50
      example: product-managers

    groupUserAssignId:
      name: groupUserAssignId
      in: path
      required: true
      description: UUID of the group assignment.
      schema:
        type: string
        format: uuid
      example: 8aacd616-0c81-4935-90d4-e00b677bd200

    

    offset:
      name: offset
      in: query
      required: false
      description: Zero-based record offset. Defaults to `0`.
      schema:
        type: integer
        minimum: 0
        default: 0
      example: 0

    limit:
      name: limit
      in: query
      required: false
      description: Number of items per page. Defaults to `25`, maximum `1000`.
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 25
      example: 25

  # ----------------------------------------------------------
  # Security Schemes
  # ----------------------------------------------------------
  securitySchemes:
    oauth2_oidc:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.gradera.ai/oauth/authorize
          tokenUrl: https://auth.gradera.ai/oauth/token
          scopes: {}
      description: >
        All requests must include a valid Bearer token issued by the OIDC provider.
        Tokens are obtained via the authorization code flow and passed in the
        `Authorization: Bearer <token>` header.

  # ----------------------------------------------------------
  # Examples
  # ----------------------------------------------------------
  examples:

    # === User examples ===
    UserInputExample:
      summary: Create or replace a user
      value:
        userName: Lou Powell
        email: lpowell@gradera.ai

    UserExample:
      summary: Single user response
      value:
        userId: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
        userName: John Doe
        email: jdoe@email.com
        _links:
          self:
            href: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
          collection:
            href: https://apis.gradera.ai/sos-admin/v1/users
          assignments:
            href: https://apis.gradera.ai/sos-admin/v1/group-assignments?userId=8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
    UserCreatedExample:
      summary: Created user response (no assignments yet)
      value:
        userId: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
        cname: niq://acme-corp/users/jdoe@email.com
        userName: John Doe
        email: jdoe@email.com
        _links:
          self:
            href: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
          collection:
            href: https://apis.gradera.ai/sos-admin/v1/users
          assignments:
            href: https://apis.gradera.ai/sos-admin/v1/group-assignments?userId=8a6dae58-5ee0-4f31-97c3-9f1b7eb48366

    UsersPageExample:
      summary: Paginated users list
      value:
        offset: 0
        limit: 25
        total: 2
        _links:
          self: https://apis.gradera.ai/sos-admin/v1/users?offset=0&limit=25
          first: https://apis.gradera.ai/sos-admin/v1/users?offset=0&limit=25
          last: https://apis.gradera.ai/sos-admin/v1/users?offset=0&limit=25
          next: null
          prev: null
        data:
          - userId: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
            cname: niq://gradera/users/lpowell@email.com
            userName: Lou Powell
            email: lpowell@email.com
            _links:
              self:
                href: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
              collection:
                href: https://apis.gradera.ai/sos-admin/v1/users
              assignments:
                href: https://apis.gradera.ai/sos-admin/v1/group-assignments?userId=8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
          - userId: b3f1cc72-9a24-4d58-8bde-7a3f0e2d1c9b
            cname: niq://acme-corp/users/fbueller@email.com
            userName: Ferris Bueller
            email: ferris@email.com
            _links:
              self:
                href: https://apis.gradera.ai/sos-admin/v1/users/b3f1cc72-9a24-4d58-8bde-7a3f0e2d1c9b
              collection:
                href: https://apis.gradera.ai/sos-admin/v1/users
              assignments:
                href: https://apis.gradera.ai/sos-admin/v1/group-assignments?userId=b3f1cc72-9a24-4d58-8bde-7a3f0e2d1c9b

    UsersListBadRequestExample:
      summary: Invalid query parameter for listing users
      value:
        - id: e3b0c442-98fc-4c14-9afb-4c6f1d2a3b4c
          code: ADM400
          title: Invalid request parameters
          detail: The `offset` parameter must be a non-negative integer.

    # === Group examples ===
    GroupInputExample:
      summary: Create or replace a group
      value:
        groupName: Product Managers
        groupSlug: product-managers
        groupDesc: >-
          Responsible for defining the product vision, strategy, and roadmap.
          Collaborates with cross-functional teams to deliver products that meet
          market needs and business goals.

    GroupExample:
      summary: Single group response
      value:
        groupName: Product Managers
        groupSlug: product-managers
        cname: niq://acme-corp/groups/product-managers
        groupDesc: >-
          Responsible for defining the product vision, strategy, and roadmap.
          Collaborates with cross-functional teams to deliver products that meet
          market needs and business goals.
        created_dts: '2026-01-15T09:00:00Z'
        updated_dts: '2026-01-15T09:00:00Z'
        _links:
          self:
            href: https://apis.gradera.ai/sos-admin/v1/groups/product-managers
          collection:
            href: https://apis.gradera.ai/sos-admin/v1/groups
          assignments:
            href: https://apis.gradera.ai/sos-admin/v1/group-assignments?groupSlug=product-managers

    GroupsPageExample:
      summary: Paginated groups list
      value:
        offset: 0
        limit: 25
        total: 2
        _links:
          self: https://apis.gradera.ai/sos-admin/v1/groups?offset=0&limit=25
          first: https://apis.gradera.ai/sos-admin/v1/groups?offset=0&limit=25
          last: https://apis.gradera.ai/sos-admin/v1/groups?offset=0&limit=25
          next: null
          prev: null
        data:
          - groupName: Product Managers
            groupSlug: product-managers
            cname: niq://acme-corp/groups/product-managers
            groupDesc: >-
              Responsible for defining the product vision, strategy, and roadmap.
              Collaborates with cross-functional teams to deliver products that meet
              market needs and business goals.
            created_dts: '2026-01-15T09:00:00Z'
            updated_dts: '2026-01-15T09:00:00Z'
            _links:
              self:
                href: https://apis.gradera.ai/sos-admin/v1/groups/product-managers
              collection:
                href: https://apis.gradera.ai/sos-admin/v1/groups
              assignments:
                href: https://apis.gradera.ai/sos-admin/v1/group-assignments?groupSlug=product-managers
          - groupName: Tech Leads
            groupSlug: tech-leads
            cname: niq://acme-corp/groups/tech-leads
            groupDesc: >-
              Focuses on creating intuitive and engaging user experiences.
              Works closely with developers to implement user-centric design solutions.
            created_dts: '2026-01-15T09:00:00Z'
            updated_dts: '2026-01-15T09:00:00Z'
            _links:
              self:
                href: https://apis.gradera.ai/sos-admin/v1/groups/tech-leads
              collection:
                href: https://apis.gradera.ai/sos-admin/v1/groups
              assignments:
                href: https://apis.gradera.ai/sos-admin/v1/group-assignments?groupSlug=tech-leads

    # === GroupAssignment examples ===
    GroupAssignmentInputExample:
      summary: Create a group assignment
      value:
        groupSlug: product-managers
        userId: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366

    GroupAssignmentExample:
      summary: Single group assignment response
      value:
        groupUserAssignId: 8aacd616-0c81-4935-90d4-e00b677bd200
        groupSlug: product-managers
        userId: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
        created_dts: '2026-02-18T12:34:56Z'
        _links:
          self:
            href: https://apis.gradera.ai/sos-admin/v1/group-assignments/8aacd616-0c81-4935-90d4-e00b677bd200
          collection:
            href: https://apis.gradera.ai/sos-admin/v1/group-assignments
          user:
            href: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
          group:
            href: https://apis.gradera.ai/sos-admin/v1/groups/product-managers

    GroupAssignmentsPageExample:
      summary: Paginated group assignments list
      value:
        offset: 0
        limit: 25
        total: 1
        _links:
          self: https://apis.gradera.ai/sos-admin/v1/group-assignments?groupSlug=product-managers&offset=0&limit=25
          first: https://apis.gradera.ai/sos-admin/v1/group-assignments?groupSlug=product-managers&offset=0&limit=25
          last: https://apis.gradera.ai/sos-admin/v1/group-assignments?groupSlug=product-managers&offset=0&limit=25
          next: null
          prev: null
        data:
          - groupUserAssignId: 8aacd616-0c81-4935-90d4-e00b677bd200
            groupSlug: product-managers
            userId: 8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
            _links:
              self:
                href: https://apis.gradera.ai/sos-admin/v1/group-assignments/8aacd616-0c81-4935-90d4-e00b677bd200
              collection:
                href: https://apis.gradera.ai/sos-admin/v1/group-assignments
              user:
                href: https://apis.gradera.ai/sos-admin/v1/users/8a6dae58-5ee0-4f31-97c3-9f1b7eb48366
              group:
                href: https://apis.gradera.ai/sos-admin/v1/groups/product-managers

    # === Error examples ===
    BadRequestExample:
      summary: Invalid request parameters
      value:
        - id: 1d2a3e4f-5b6c-7d8e-9f0a-1b2c3d4e5f6a
          code: ADM400
          title: Invalid request parameters
          detail: The `email` field must be a valid email address.

    UnauthorizedExample:
      summary: Authentication required
      value:
        - id: 2e3b4c5d-6a7f-8e9d-0b1c-2d3e4f5a6b7c
          code: ADM401
          title: Authentication required
          detail: You must provide a valid OAuth 2.0 Bearer token to access this resource.

    ForbiddenExample:
      summary: Access denied
      value:
        - id: 3f4c5d6e-7b8a-9f0e-1c2d-3e4f5a6b7c8d
          code: ADM403
          title: Access denied
          detail: You do not have permission to perform this operation.

    NotFoundExample:
      summary: Resource not found
      value:
        - id: 4a5b6c7d-8e9f-0a1b-2c3d-4e5f6a7b8c9d
          code: ADM404
          title: Resource not found
          detail: The requested resource does not exist or is not accessible.

    ConflictExample:
      summary: Resource conflict
      value:
        - id: 5b6c7d8e-9f0a-1b2c-3d4e-5f6a7b8c9d0e
          code: ADM409
          title: Resource conflict
          detail: "Cannot delete group: users are still assigned."

    UnsupportedMediaTypeExample:
      summary: Unsupported media type
      value:
        - id: 6c7d8e9f-0a1b-2c3d-4e5f-6a7b8c9d0e1f
          code: ADM415
          title: Unsupported media type
          detail: The request content type is not supported. Please use 'application/json'.

    RequestLimitExceededExample:
      summary: Rate limit exceeded
      value:
        - id: 7d8e9f0a-1b2c-3d4e-5f6a-7b8c9d0e1f2a
          code: ADM429
          title: Rate limit exceeded
          detail: You have exceeded the allowed number of requests. Please wait and try again later.

    InternalServiceErrorExample:
      summary: Internal server error
      value:
        - id: 8e9f0a1b-2c3d-4e5f-6a7b-8c9d0e1f2a3b
          code: ADM500
          title: Internal server error
          detail: An unexpected error occurred while processing your request. Please try again later or contact support.

    ServiceUnavailableExample:
      summary: Service unavailable
      value:
        - id: 9f0a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c
          code: ADM503
          title: Service unavailable
          detail: The service is temporarily unavailable. Please try again later