diff --git a/gms/README.md b/gms/README.md new file mode 100644 index 00000000..c509f0d3 --- /dev/null +++ b/gms/README.md @@ -0,0 +1,93 @@ +# Groups Management Service (gms) + +The ***gms*** service provides RESTful web service interface for group management operations. It handles creation, modification, deletion, and querying of user groups, as well as managing group memberships and permissions. + +This service works with a user service that provides user identity information. TBD + +## deployment +The `gms` war file can be renamed at deployment time in order to support an alternate service name, including +introducing additional path elements. +See cadc-tomcat (war-rename.conf). + +## configuration +The following runtime configuration must be made available via the `/config` directory. + +### catalina.properties +This file contains java system properties to configure the tomcat server and some of the java libraries +used in the service. + +See cadc-tomcat for +system properties related to the deployment environment. + +See cadc-util for common system properties. + +### cadc-registry.properties + +See cadc-registry. + +### gms.properties +TBD. At minimum, the following properties are required: +- Corresponding user service base URL and the name of the user identity attribute. + +## API Overview + +The GMS service provides the following operations: + +### Group Management +- **List all groups** - GET /groups +- **Create group** - PUT /groups +- **Get group** - GET /groups/{groupName} +- **Delete group** - DELETE /groups/{groupName} +- **Modify group** - POST /groups/{groupName} +- **Add/Remove user members** - POST/DELETE /groups/{groupName}/userMembers +- **Add/Remove group members** - POST/DELETE /groups/{groupName}/groupMembers/{groupName2} (Is it required to distinguish between user and group members?) + +### Group Searching +- **Search by role** - GET /search?id={userID}&idType={idType}&role={role} +- **Search specific membership** - GET /search?group={groupIVOID} (repeatable; see OpenAPI) + +### Authentication Methods +The service supports multiple authentication methods: +- **Client certificates** (CC) over HTTPS - `/groups/*` endpoints +- **Anonymous** (AN) access for listing operations + +## Group Structure + +Groups have the following key components: +- **Owner** - Can modify administrator/member lists and delete the group +- **Administrators** - Can modify administrator and member lists +- **Members** - Are granted access to resources the group is associated with + +Both users and other groups can be members or administrators of a group. + +## building it +``` +gradle clean build +docker build -t gms -f Dockerfile . +``` + +## running it +``` +docker run --rm --user tomcat:tomcat --volume=/path/to/external/config:/config:ro --name gms gms:latest +``` + +## testing it + +### Unit tests +``` +gradle clean test +``` + +### Integration tests +``` +gradle clean intTest +``` + +For local testing against a running instance: +``` +~/bin/int-test-localhost.sh +``` + +## API Documentation + +For detailed API specifications including request/response formats, authentication requirements, and error codes, see the OpenAPI specification or the service capabilities endpoint at `/capabilities`. diff --git a/gms/model.puml b/gms/model.puml new file mode 100644 index 00000000..2d8fa212 --- /dev/null +++ b/gms/model.puml @@ -0,0 +1,65 @@ +@startuml GMS Domain Model + +class Group { + - uri: URI + - description: String + - gid: Integer + - lastModified: Date + - owner: User + - userMembers: Set + - userAdmins: Set + - groupMembers: Set + - groupAdmins: Set +} + +class User { + - identity: Identity + - firstName: String + - lastName: String + - email: String +} + +class Identity { + - type: String + - value: String +} + +enum IdentityType { + X500 + OPENID + NUMERIC +} + +enum Role { + OWNER + ADMIN + MEMBER +} + +' Relationships +Group "1" *-- "1" User : owner +Group "1" o-- "0..*" User : userMembers +Group "1" o-- "0..*" User : userAdmins +Group "1" o-- "0..*" Group : groupMembers +Group "1" o-- "0..*" Group : groupAdmins + +User "1" *-- "1" Identity : key identity + +Identity ..> IdentityType : <> +Group ..> Role : <> + +note right of Group + A group can contain both + users and other groups as + members or administrators +end note + +note left of User + Users are identified by + a configurable identity + (primary key) that the + corresponding user service + provides +end note + +@enduml diff --git a/gms/src/main/webapp/openapi.yaml b/gms/src/main/webapp/openapi.yaml new file mode 100644 index 00000000..bb28388c --- /dev/null +++ b/gms/src/main/webapp/openapi.yaml @@ -0,0 +1,59 @@ +## GMS OpenAPI root: path items under ./openapi/; shared parameters/schemas in ./openapi/components.yaml; standard HTTP error fragments in ./openapi/vosi/vosi-std-responses.yaml (avoid `$ref` from fragments back into this file — that can deadlock libopenapi/vacuum). +openapi: 3.1.0 +info: + title: CANFAR Group Management System (GMS) API + description: | + This API provides RESTful access to group management operations in the CANFAR Group Management System. + + Groups have an owner, a list of administrators, and a list of members. The list of members and + administrators can be composed of groups and users. + + **Roles:** + - **owner** - Can modify administrators list and members list. Can delete the group. + - **admin** - Can modify the administrators and members list. + - **member** - Are granted access to the resources the group is associated with. + + **Authentication Methods:** + - **CC** - Client certificates over HTTPS + - **JWT** - JWT Bearer token over HTTPS + - **AN** - Anonymous over HTTP + + **User Identity:** + Users are identified by their unique userID in the Identity Provider (CANFAR HTTP username). + version: 1.0.0 + contact: + url: https://www.canfar.net +tags: + - name: Group Management + description: Operations for managing groups + - name: GMS + description: Operations for searching groups by user role (IVOA GMS 1.0) + - name: Group Membership + description: GMS membership and identity-related queries + - name: VOSI Capabilities + description: VOSI capability and probe operations + - name: Service availability + description: Service availability + - name: POSIX Groups + description: POSIX group id (gid) and group name mapping +servers: + - url: /gms + +paths: + /capabilities: + $ref: ./openapi/vosi/vosi-capabilities.yaml + /availability: + $ref: ./openapi/cadcvosi/availability.yaml + /groups: + $ref: ./openapi/service/group.yaml#/GroupsCollection + /groups/{groupName}: + $ref: ./openapi/service/group.yaml#/GroupById + /groups/{groupName}/userMembers/{userID}: + $ref: ./openapi/service/group.yaml#/GroupUserMembers + /groups/{groupName}/groupMembers/{groupName2}: + $ref: ./openapi/service/group.yaml#/GroupGroupMembers + /search: + $ref: ./openapi/gms/gms-search.yaml + /gidmap: + $ref: ./openapi/pm/posix-groupmap.yaml + diff --git a/gms/src/main/webapp/openapi/cadcvosi/availability.yaml b/gms/src/main/webapp/openapi/cadcvosi/availability.yaml new file mode 100644 index 00000000..e4887f37 --- /dev/null +++ b/gms/src/main/webapp/openapi/cadcvosi/availability.yaml @@ -0,0 +1,31 @@ +get: + operationId: read-availability + tags: + - Service availability + summary: Returns the availability status of the service + description: Get the set of descriptions for this service. + responses: + '200': + description: Service is available + content: + application/xml: + schema: + type: object + xml: + name: availability + properties: + available: + type: boolean + example: true + xml: + name: availability + prefix: vosi + namespace: http://www.ivoa.net/xml/VOSIAvailability/v1.0 + note: + type: string + example: service is accepting requests + example: + available: true + note: service is accepting requests + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable diff --git a/gms/src/main/webapp/openapi/components.yaml b/gms/src/main/webapp/openapi/components.yaml new file mode 100644 index 00000000..47be1074 --- /dev/null +++ b/gms/src/main/webapp/openapi/components.yaml @@ -0,0 +1,88 @@ +# Shared components for the GMS OpenAPI description (referenced from path fragments and the root document). +components: + parameters: + groupName: + name: groupName + in: path + required: true + description: | + The group name (simple identifier), e.g. teamA, myGroup + schema: + type: string + example: teamA + userID: + name: userID + in: path + required: true + description: The unique user identifier (CANFAR HTTP username) + schema: + type: string + example: user1 + idType: + name: idType + in: query + description: The type of userID (http, x509, cadc) + required: true + schema: + type: string + enum: + - http + - x509 + - cadc + example: http + + schemas: + Group: + type: object + description: Representation of a group + User: + type: string + description: User identifier (CANFAR HTTP username) + example: "exampleuser" + + examples: + example-group: + summary: Example group document (JSON/XML body shape) + value: + uri: "ivo://cadc.nrc.ca/gms#teamA" + owner: "owner1" + description: "Research team access group" + lastModified: "2015-07-24T17:38:18.000" + userMembers: + - "member1" + - "member2" + - "member3" + userAdmins: + - "admin1" + - "admin2" + groupMembers: + - uri: "ivo://cadc.nrc.ca/gms#shared-data" + - uri: "ivo://cadc.nrc.ca/gms#pipeline-users" + groupAdmins: + - uri: "ivo://cadc.nrc.ca/gms#platform-admins" + + GroupXmlDetailed: + summary: Detailed group example (XML) + value: | + + ivo://cadc.nrc.ca/gms#teamA + owner1 + Research team access group + 2015-07-24T17:38:18.000 + + member1 + member2 + member3 + + + admin1 + admin2 + + + ivo://cadc.nrc.ca/gms#shared-data + ivo://cadc.nrc.ca/gms#pipeline-users + + + ivo://cadc.nrc.ca/gms#platform-admins + + diff --git a/gms/src/main/webapp/openapi/gms/gms-search.yaml b/gms/src/main/webapp/openapi/gms/gms-search.yaml new file mode 100644 index 00000000..3f9c99cf --- /dev/null +++ b/gms/src/main/webapp/openapi/gms/gms-search.yaml @@ -0,0 +1,83 @@ +get: + tags: + - GMS + summary: Query group membership + description: | + Returns group memberships for the authenticated identity. + + If one or more `group` parameters are supplied, the response SHALL + contain only those groups for which membership is confirmed. + + If no `group` parameter is supplied, the response SHALL contain all + groups of which the identity is a member. + + The response SHALL be plain text containing zero or more lines, + each line containing a simple group name. + operationId: getMembership + + parameters: + - name: group + in: query + description: Membership of one or more groups. + required: false + schema: + type: array + items: + type: string + example: + - "group1" + - "group2" + - "group3" + + responses: + '200': + description: Successful membership response + content: + text/plain: + schema: + type: string + description: | + Plain text response listing simple group names, one per line. + examples: + filtered: + summary: Filtered by requested groups + value: | + groupA + groupC + allGroups: + summary: All memberships (no group parameter supplied) + value: | + groupA + groupB + groupC + none: + summary: No memberships + value: "" + + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + +components: + + parameters: + + group: + name: group + in: query + description: | + Name of a group. + + This parameter MAY be repeated to test membership in multiple groups. + + If omitted, all group memberships SHALL be returned. + required: false + example: + - ivo://example.org/gms?engineering + schema: + type: array + items: + type: string + example: ivo://example.org/gms?engineering + style: form \ No newline at end of file diff --git a/gms/src/main/webapp/openapi/pm/posix-groupmap.yaml b/gms/src/main/webapp/openapi/pm/posix-groupmap.yaml new file mode 100644 index 00000000..c5106d50 --- /dev/null +++ b/gms/src/main/webapp/openapi/pm/posix-groupmap.yaml @@ -0,0 +1,125 @@ +get: + operationId: get-groupmap + tags: + - POSIX Groups + summary: get local posix info for groups + parameters: + - name: group + in: query + description: request only specified group records + required: false + schema: + type: array + items: + type: string + example: + - "ivo://foo.net/gms?group1" + - "ivo://foo.net/gms?group2" + - "ivo://bar.net/gms?group1" + - name: gid + in: query + description: request group record(s) by posix gid + schema: + type: array + items: + type: integer + example: + - 10001 + - 10002 + - 20001 + - 20002 + responses: + '200': + $ref: '#/components/responses/PosixGroups' + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + $ref: ../vosi/vosi-std-responses.yaml#/not-found +post: + operationId: create-groupmap-entry + tags: + - POSIX Groups + summary: create a local posix info for a group + parameters: + - name: group + in: query + description: | + create and return a posix group entry for the specified group; + the value is an IVOA Group URI as defined by the GMS standard + required: false + schema: + type: string + example: "ivo://foo.net/gms?group1" + responses: + '201': + $ref: '#/components/responses/PosixGroups' + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + $ref: ../vosi/vosi-std-responses.yaml#/not-found + '405': + $ref: ../vosi/vosi-std-responses.yaml#/not-implemented + +components: + responses: + PosixGroups: + description: list of groups with posix details + content: + text/plain: + description: posix groups in /etc/group format + schema: + type: string + example: | + foo_group1:x:20001: + foo_group2:x:20002 + bar_group1:x:20003 + application/x-posix-group+json: + description: POSIX group details as JSON array + schema: + type: array + example: + - uri: "ivo://foo.net/gms?group1" + posix: + name: foo_group1 + gid: 20001 + - uri: "ivo://foo.net/gms?group2" + posix: + name: foo_group2 + gid: 20002 + - uri: "ivo://bar.net/gms?group1" + posix: + name: bar_group1 + gid: 20003 + items: + type: object + example: + uri: "ivo://foo.net/gms?group1" + posix: + name: foo_group1 + gid: 20001 + required: + - uri + - posix + properties: + uri: + type: string + example: "ivo://foo.net/gms?group1" + posix: + type: object + example: + name: foo_group1 + gid: 20001 + required: + - name + - gid + properties: + name: + type: string + example: foo_group1 + gid: + type: integer + example: 20001 \ No newline at end of file diff --git a/gms/src/main/webapp/openapi/service/group.yaml b/gms/src/main/webapp/openapi/service/group.yaml new file mode 100644 index 00000000..b3aedf1e --- /dev/null +++ b/gms/src/main/webapp/openapi/service/group.yaml @@ -0,0 +1,260 @@ +# Path items for /groups and nested routes. Fragment keys are referenced from root `openapi.yaml` `paths` (Path Item objects only — no root-level `components` here). +GroupsCollection: + get: + summary: List all groups + description: Lists the names of all the groups in the service + operationId: listGroups + tags: + - Group Management + responses: + '200': + description: Successful response with list of group names + content: + text/plain: + schema: + type: string + description: Newline-separated list of group names + example: | + group1 + group2 + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + + put: + summary: Create group + description: Create a new group according to the group XML or JSON document in the HTTP PUT + operationId: createGroup + tags: + - Group Management + requestBody: + description: Group data in XML or JSON format + required: true + content: + application/xml: + schema: + $ref: ../components.yaml#/components/schemas/Group + examples: + example-group: + $ref: ../components.yaml#/components/examples/example-group + application/json: + schema: + $ref: ../components.yaml#/components/schemas/Group + examples: + example-group: + $ref: ../components.yaml#/components/examples/example-group + responses: + '200': + description: Group created successfully + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '404': + description: A member is not recognized + '409': + description: A group with the same name already exists + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + +GroupById: + parameters: + - $ref: ../components.yaml#/components/parameters/groupName + get: + summary: Get group + description: Get the group with the specified groupName. Only users associated with the group (owner, admin, member) + are allowed to retrieve the group details. + operationId: getGroup + tags: + - Group Management + responses: + '200': + description: Successful response with group details + content: + application/xml: + schema: + $ref: ../components.yaml#/components/schemas/Group + examples: + example-group: + $ref: ../components.yaml#/components/examples/example-group + application/json: + schema: + $ref: ../components.yaml#/components/schemas/Group + examples: + example-group: + $ref: ../components.yaml#/components/examples/example-group + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + description: The group could not be found + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + + post: + summary: Modify group + description: Modify the group with the specified groupName according to the group XML or JSON document in the HTTP POST + Only the owner or administrators of the group are allowed to modify the group. + operationId: modifyGroup + tags: + - Group Management + requestBody: + description: Updated group data in XML or JSON format + required: true + content: + application/xml: + schema: + $ref: ../components.yaml#/components/schemas/Group + examples: + example-group: + $ref: ../components.yaml#/components/examples/example-group + application/json: + schema: + $ref: ../components.yaml#/components/schemas/Group + examples: + example-group: + $ref: ../components.yaml#/components/examples/example-group + responses: + '200': + description: Group modified successfully + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + description: The group could not be found or a member is not recognized + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + + delete: + summary: Delete group + description: Delete the group with the specified groupName. Only the owner of the group is allowed to delete the group. + operationId: deleteGroup + tags: + - Group Management + responses: + '200': + description: Group deleted successfully + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + description: The group to delete could not be found + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + +GroupUserMembers: + parameters: + - $ref: ../components.yaml#/components/parameters/groupName + - $ref: ../components.yaml#/components/parameters/userID + - $ref: ../components.yaml#/components/parameters/idType + + put: + summary: Add user member + description: Add a user as a member of the specified group. Only owners or administrators of the group are + allowed to add members. + operationId: addUserMember + tags: + - Group Management + responses: + '200': + description: User added as member successfully + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + description: The group to add to could not be found or the member is not recognized + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + + delete: + summary: Remove user member + description: Remove a user as a member of the specified group. Only owners or administrators of the group are + allowed to remove members. + operationId: removeUserMember + tags: + - Group Management + responses: + '200': + description: User removed successfully + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + description: The group could not be found or the member is not recognized + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + +GroupGroupMembers: + parameters: + - $ref: ../components.yaml#/components/parameters/groupName + - name: groupName2 + in: path + required: true + description: | + The member group name to add or remove (e.g. `teamA`, `ABCD`). Only the group owner or administrators can + add or remove group members. + schema: + type: string + + put: + summary: Add group member + description: Add a group as a member of another group. Only internal groups can be added as members. + operationId: addGroupMember + tags: + - Group Management + responses: + '200': + description: Group member added successfully + '400': + $ref: ../vosi/vosi-std-responses.yaml#/bad-request + '401': + $ref: ../vosi/vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ../vosi/vosi-std-responses.yaml#/permission-denied + '404': + description: One or both groups could not be found + '500': + $ref: ../vosi/vosi-std-responses.yaml#/internal-server-error + '503': + $ref: ../vosi/vosi-std-responses.yaml#/service-unavailable + + delete: + summary: Remove group member + description: Remove a group as a member of another group. Only the group owner or administrators can remove + group members. + operationId: removeGroupMember + tags: + - Group Management + responses: + '200': + description: Group member removed successfully diff --git a/gms/src/main/webapp/openapi/vosi/vosi-capabilities.yaml b/gms/src/main/webapp/openapi/vosi/vosi-capabilities.yaml new file mode 100644 index 00000000..a35bf5dd --- /dev/null +++ b/gms/src/main/webapp/openapi/vosi/vosi-capabilities.yaml @@ -0,0 +1,40 @@ +# Path item for /capabilities (VOSI). Must be a Path Item object only (no root-level `components` here). +head: + operationId: probe-capabilities + tags: + - VOSI Capabilities + summary: VOSI Capabilities with SSO-next prototype + description: | + Get standard headers for the service. The standard headers should include + AAI related headers (www-authenticate challenges or x-vo-authenticated + indicator) if applicable, server name and version info, and other + applicable header information. SSO-next: clients use this operation to probe + for available authentication methods. + responses: + '200': + $ref: ./vosi-std-responses.yaml#/authenticated + '401': + $ref: ./vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ./vosi-std-responses.yaml#/permission-denied + +get: + operationId: read-capabilities + tags: + - VOSI Capabilities + summary: VOSI Capabilities with SSO-next prototype + description: Get the set of VOResource capability descriptions for this service. + responses: + '200': + description: VOSI capabilities document + content: + text/xml: + schema: + type: string + example: | + + + '401': + $ref: ./vosi-std-responses.yaml#/not-authenticated + '403': + $ref: ./vosi-std-responses.yaml#/permission-denied diff --git a/gms/src/main/webapp/openapi/vosi/vosi-std-responses.yaml b/gms/src/main/webapp/openapi/vosi/vosi-std-responses.yaml new file mode 100644 index 00000000..32f12e64 --- /dev/null +++ b/gms/src/main/webapp/openapi/vosi/vosi-std-responses.yaml @@ -0,0 +1,66 @@ +# standard +authenticated: + description: a response header indicating that authentication was successful + headers: + x-vo-authenticated: + schema: + type: string + +not-authenticated: + description: | + Authentication is required and the current attempt failed, or credentials were not provided. + headers: + www-authenticate: + schema: + type: string + +permission-denied: + description: | + Caller is not authorized to perform the operation (forbidden / permission denied). + content: + text/plain: + schema: + type: string + +bad-request: + description: | + Invalid request or input; the URL or request body may be malformed. + content: + text/plain: + schema: + type: string + +not-found: + description: resource not found + content: + text/plain: + schema: + type: string + +not-implemented: + description: this response indicates that the service does not support this operation + content: + text/plain: + schema: + type: string + +too-large: + description: the request content exceeds a server-side limitation on size + content: + text/plain: + schema: + type: string + +internal-server-error: + description: the server encountered an unrecoverable error + content: + text/plain: + schema: + type: string + +service-unavailable: + description: the server is too busy to perform the operation + content: + text/plain: + schema: + type: string