From 8ec27ff99dbfd2186cb51174c86d3c375ee38a37 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Thu, 19 Sep 2024 16:49:55 +0000 Subject: [PATCH 01/11] add and list group members Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 82 +++++++++ internal/api/httpsrv/server.gen.go | 165 ++++++++++++++++++ internal/storage/groups.go | 95 ++++++++++ .../migrations/0005_group_membership.sql | 11 ++ internal/types/groups.go | 5 + openapi-v1.yaml | 97 ++++++++-- pkg/api/v1/paginate_group_members.go | 44 +++++ pkg/api/v1/types.gen.go | 125 +++++++------ 8 files changed, 552 insertions(+), 72 deletions(-) create mode 100644 internal/api/httpsrv/handler_group_members.go create mode 100644 internal/storage/migrations/0005_group_membership.sql create mode 100644 pkg/api/v1/paginate_group_members.go diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go new file mode 100644 index 00000000..e4db74cf --- /dev/null +++ b/internal/api/httpsrv/handler_group_members.go @@ -0,0 +1,82 @@ +package httpsrv + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "go.infratographer.com/identity-api/internal/types" + v1 "go.infratographer.com/identity-api/pkg/api/v1" + "go.infratographer.com/permissions-api/pkg/permissions" + "go.infratographer.com/x/gidx" +) + +// AddGroupMembers creates a group +func (h *apiHandler) AddGroupMembers(ctx context.Context, req AddGroupMembersRequestObject) (AddGroupMembersResponseObject, error) { + reqbody := req.Body + gid := req.GroupID + + if _, err := gidx.Parse(string(gid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid owner id: %s", err.Error()), + ) + + return nil, err + } + + if err := permissions.CheckAccess(ctx, gid, actionGroupUpdate); err != nil { + return nil, permissionsError(err) + } + + if err := h.engine.AddMembers(ctx, gid, reqbody.MemberIds...); err != nil { + if errors.Is(err, types.ErrNotFound) { + err = echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return nil, err + } + + return AddGroupMembers200JSONResponse{Ok: true}, nil +} + +// ListGroupMembers lists the members of a group +func (h *apiHandler) ListGroupMembers(ctx context.Context, req ListGroupMembersRequestObject) (ListGroupMembersResponseObject, error) { + gid := req.GroupID + + if _, err := gidx.Parse(string(gid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid group id: %s", err.Error()), + ) + + return nil, err + } + + if err := permissions.CheckAccess(ctx, gid, actionGroupGet); err != nil { + return nil, permissionsError(err) + } + + members, err := h.engine.ListMembers(ctx, gid, req.Params) + if err != nil { + if errors.Is(err, types.ErrNotFound) { + err = echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return nil, err + } + + collection := v1.GroupMemberCollection{ + Members: members, + GroupID: gid, + Pagination: v1.Pagination{}, + } + + if err := req.Params.SetPagination(&collection); err != nil { + return nil, err + } + + return ListGroupMembers200JSONResponse{GroupMemberCollectionJSONResponse(collection)}, nil +} diff --git a/internal/api/httpsrv/server.gen.go b/internal/api/httpsrv/server.gen.go index 712017b9..b8e310e6 100644 --- a/internal/api/httpsrv/server.gen.go +++ b/internal/api/httpsrv/server.gen.go @@ -33,6 +33,12 @@ type ServerInterface interface { // Updates a Group // (PATCH /api/v1/groups/{groupID}) UpdateGroup(ctx echo.Context, groupID GroupID) error + // Gets members of a Group + // (GET /api/v1/groups/{groupID}/members) + ListGroupMembers(ctx echo.Context, groupID GroupID, params ListGroupMembersParams) error + // Adds a member to a Group + // (POST /api/v1/groups/{groupID}/members) + AddGroupMembers(ctx echo.Context, groupID GroupID) error // Deletes an issuer with the given ID. // (DELETE /api/v1/issuers/{id}) DeleteIssuer(ctx echo.Context, id gidx.PrefixedID) error @@ -153,6 +159,54 @@ func (w *ServerInterfaceWrapper) UpdateGroup(ctx echo.Context) error { return err } +// ListGroupMembers converts echo context to params. +func (w *ServerInterfaceWrapper) ListGroupMembers(ctx echo.Context) error { + var err error + // ------------- Path parameter "groupID" ------------- + var groupID GroupID + + err = runtime.BindStyledParameterWithOptions("simple", "groupID", ctx.Param("groupID"), &groupID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter groupID: %s", err)) + } + + // Parameter object where we will unmarshal all parameters from the context + var params ListGroupMembersParams + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", ctx.QueryParams(), ¶ms.Cursor) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter cursor: %s", err)) + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ListGroupMembers(ctx, groupID, params) + return err +} + +// AddGroupMembers converts echo context to params. +func (w *ServerInterfaceWrapper) AddGroupMembers(ctx echo.Context) error { + var err error + // ------------- Path parameter "groupID" ------------- + var groupID GroupID + + err = runtime.BindStyledParameterWithOptions("simple", "groupID", ctx.Param("groupID"), &groupID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter groupID: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.AddGroupMembers(ctx, groupID) + return err +} + // DeleteIssuer converts echo context to params. func (w *ServerInterfaceWrapper) DeleteIssuer(ctx echo.Context) error { var err error @@ -426,6 +480,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.DELETE(baseURL+"/api/v1/groups/:groupID", wrapper.DeleteGroup) router.GET(baseURL+"/api/v1/groups/:groupID", wrapper.GetGroupByID) router.PATCH(baseURL+"/api/v1/groups/:groupID", wrapper.UpdateGroup) + router.GET(baseURL+"/api/v1/groups/:groupID/members", wrapper.ListGroupMembers) + router.POST(baseURL+"/api/v1/groups/:groupID/members", wrapper.AddGroupMembers) router.DELETE(baseURL+"/api/v1/issuers/:id", wrapper.DeleteIssuer) router.GET(baseURL+"/api/v1/issuers/:id", wrapper.GetIssuerByID) router.PATCH(baseURL+"/api/v1/issuers/:id", wrapper.UpdateIssuer) @@ -447,6 +503,14 @@ type GroupCollectionJSONResponse struct { Pagination Pagination `json:"pagination"` } +type GroupMemberCollectionJSONResponse struct { + GroupID gidx.PrefixedID `json:"group_id"` + Members []gidx.PrefixedID `json:"members"` + + // Pagination collection response pagination + Pagination Pagination `json:"pagination"` +} + type IssuerCollectionJSONResponse struct { Issuers []Issuer `json:"issuers"` @@ -553,6 +617,44 @@ func (response UpdateGroup200JSONResponse) VisitUpdateGroupResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type ListGroupMembersRequestObject struct { + GroupID GroupID `json:"groupID"` + Params ListGroupMembersParams +} + +type ListGroupMembersResponseObject interface { + VisitListGroupMembersResponse(w http.ResponseWriter) error +} + +type ListGroupMembers200JSONResponse struct { + GroupMemberCollectionJSONResponse +} + +func (response ListGroupMembers200JSONResponse) VisitListGroupMembersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type AddGroupMembersRequestObject struct { + GroupID GroupID `json:"groupID"` + Body *AddGroupMembersJSONRequestBody +} + +type AddGroupMembersResponseObject interface { + VisitAddGroupMembersResponse(w http.ResponseWriter) error +} + +type AddGroupMembers200JSONResponse AddGroupMembersResponse + +func (response AddGroupMembers200JSONResponse) VisitAddGroupMembersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + type DeleteIssuerRequestObject struct { Id gidx.PrefixedID `json:"id"` } @@ -767,6 +869,12 @@ type StrictServerInterface interface { // Updates a Group // (PATCH /api/v1/groups/{groupID}) UpdateGroup(ctx context.Context, request UpdateGroupRequestObject) (UpdateGroupResponseObject, error) + // Gets members of a Group + // (GET /api/v1/groups/{groupID}/members) + ListGroupMembers(ctx context.Context, request ListGroupMembersRequestObject) (ListGroupMembersResponseObject, error) + // Adds a member to a Group + // (POST /api/v1/groups/{groupID}/members) + AddGroupMembers(ctx context.Context, request AddGroupMembersRequestObject) (AddGroupMembersResponseObject, error) // Deletes an issuer with the given ID. // (DELETE /api/v1/issuers/{id}) DeleteIssuer(ctx context.Context, request DeleteIssuerRequestObject) (DeleteIssuerResponseObject, error) @@ -945,6 +1053,63 @@ func (sh *strictHandler) UpdateGroup(ctx echo.Context, groupID GroupID) error { return nil } +// ListGroupMembers operation middleware +func (sh *strictHandler) ListGroupMembers(ctx echo.Context, groupID GroupID, params ListGroupMembersParams) error { + var request ListGroupMembersRequestObject + + request.GroupID = groupID + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ListGroupMembers(ctx.Request().Context(), request.(ListGroupMembersRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListGroupMembers") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ListGroupMembersResponseObject); ok { + return validResponse.VisitListGroupMembersResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// AddGroupMembers operation middleware +func (sh *strictHandler) AddGroupMembers(ctx echo.Context, groupID GroupID) error { + var request AddGroupMembersRequestObject + + request.GroupID = groupID + + var body AddGroupMembersJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.AddGroupMembers(ctx.Request().Context(), request.(AddGroupMembersRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "AddGroupMembers") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(AddGroupMembersResponseObject); ok { + return validResponse.VisitAddGroupMembersResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // DeleteIssuer operation middleware func (sh *strictHandler) DeleteIssuer(ctx echo.Context, id gidx.PrefixedID) error { var request DeleteIssuerRequestObject diff --git a/internal/storage/groups.go b/internal/storage/groups.go index 01005142..df9eeca4 100644 --- a/internal/storage/groups.go +++ b/internal/storage/groups.go @@ -24,6 +24,14 @@ var groupCols = struct { Description: "description", } +var groupMemberCols = struct { + GroupID string + SubjectID string +}{ + GroupID: "group_id", + SubjectID: "subject_id", +} + var groupColsStr = strings.Join([]string{ groupCols.ID, groupCols.OwnerID, groupCols.Name, groupCols.Description, @@ -227,3 +235,90 @@ func (gs *groupService) DeleteGroup(ctx context.Context, id gidx.PrefixedID) err return err } + +func (gs *groupService) AddMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error { + if len(subjects) == 0 { + return nil + } + + tx, err := getContextTx(ctx) + if err != nil { + return err + } + + if _, err := gs.fetchGroupByID(ctx, groupID); err != nil { + return err + } + + vals := make([]string, 0, len(subjects)) + + for _, subj := range subjects { + vals = append(vals, fmt.Sprintf("('%s', '%s')", groupID, subj)) + } + + q := fmt.Sprintf( + "UPSERT INTO group_members (%s, %s) VALUES %s", + groupMemberCols.GroupID, groupMemberCols.SubjectID, + strings.Join(vals, ", "), + ) + + _, err = tx.ExecContext(ctx, q) + if err != nil { + fmt.Println(err.Error()) + } + + return err +} + +func (gs *groupService) ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) { + paginate := crdbx.Paginate(pagination, crdbx.ContextAsOfSystemTime(ctx, nil)) + + q := fmt.Sprintf( + "SELECT %s FROM group_members %s WHERE %s = $1 %s %s %s", + groupMemberCols.SubjectID, paginate.AsOfSystemTime(), groupMemberCols.GroupID, + paginate.AndWhere(2), //nolint:gomnd + paginate.OrderClause(), + paginate.LimitClause(), + ) + + rows, err := gs.db.QueryContext(ctx, q, paginate.Values(groupID)...) + if err != nil { + return nil, err + } + + defer rows.Close() + + var members []gidx.PrefixedID + + for rows.Next() { + var member gidx.PrefixedID + + if err := rows.Scan(&member); err != nil { + return nil, err + } + + members = append(members, member) + } + + return members, nil +} + +func (gs *groupService) RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subjectID gidx.PrefixedID) error { + tx, err := getContextTx(ctx) + if err != nil { + return err + } + + if _, err := gs.fetchGroupByID(ctx, groupID); err != nil { + return err + } + + q := fmt.Sprintf( + "DELETE FROM group_members WHERE %s = $1 AND %s = $2", + groupMemberCols.GroupID, groupMemberCols.SubjectID, + ) + + _, err = tx.ExecContext(ctx, q, groupID, subjectID) + + return err +} diff --git a/internal/storage/migrations/0005_group_membership.sql b/internal/storage/migrations/0005_group_membership.sql new file mode 100644 index 00000000..4d71e101 --- /dev/null +++ b/internal/storage/migrations/0005_group_membership.sql @@ -0,0 +1,11 @@ +-- +goose Up +CREATE TABLE group_members ( + group_id STRING NOT NULL REFERENCES groups(id), + subject_id STRING NOT NULL, + + index group_memberships_subject_id_index (subject_id), + primary key (group_id, subject_id) +); + +-- +goose Down +DROP TABLE group_members; diff --git a/internal/types/groups.go b/internal/types/groups.go index baae58af..6af3d31b 100644 --- a/internal/types/groups.go +++ b/internal/types/groups.go @@ -48,6 +48,11 @@ type GroupService interface { ListGroups(ctx context.Context, ownerID gidx.PrefixedID, pagination crdbx.Paginator) (Groups, error) UpdateGroup(ctx context.Context, id gidx.PrefixedID, update GroupUpdate) (*Group, error) DeleteGroup(ctx context.Context, id gidx.PrefixedID) error + + AddMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error + ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) + // RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subjectID gidx.PrefixedID) error + // ReplaceMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error } // Groups represents a list of groups diff --git a/openapi-v1.yaml b/openapi-v1.yaml index 2e78fe7b..e91b4bed 100644 --- a/openapi-v1.yaml +++ b/openapi-v1.yaml @@ -334,6 +334,43 @@ paths: application/json: schema: $ref: '#/components/schemas/Group' + + /api/v1/groups/{groupID}/members: + get: + tags: + - Groups + summary: Gets members of a Group + description: Gets the members of a group by ID. + operationId: listGroupMembers + parameters: + - $ref: '#/components/parameters/groupID' + - $ref: '#/components/parameters/pageCursor' + - $ref: '#/components/parameters/pageLimit' + responses: + '200': + $ref: '#/components/responses/GroupMemberCollection' + post: + tags: + - Groups + summary: Adds a member to a Group + description: Adds a member to a group by ID. + operationId: addGroupMembers + parameters: + - $ref: '#/components/parameters/groupID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddGroupMembers' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AddGroupMembersResponse' + components: schemas: DeleteResponse: @@ -522,7 +559,6 @@ components: - id - name - owner - - members properties: id: x-go-name: ID @@ -540,28 +576,25 @@ components: type: string x-go-type: gidx.PrefixedID description: ID of the owner of the group - members: + + AddGroupMembers: + required: + - member_ids + properties: + member_ids: type: array items: type: string x-go-type: gidx.PrefixedID - description: IDs of the members of the group - created_at: - type: string - format: date-time - description: time the group was created - created_by: - type: string - x-go-type: gidx.PrefixedID - description: ID of the user who created the group - updated_at: - type: string - format: date-time - description: time the group was last updated - updated_by: - type: string - x-go-type: gidx.PrefixedID - description: ID of the user who last updated the group + description: IDs of the members to add to the group + + AddGroupMembersResponse: + required: + - ok + properties: + ok: + type: boolean + description: true if the members were added successfully parameters: ownerID: @@ -684,3 +717,29 @@ components: $ref: '#/components/schemas/Group' pagination: $ref: '#/components/schemas/Pagination' + GroupMemberCollection: + description: a collection of group members + content: + application/json: + schema: + type: object + required: + - members + - group_id + - pagination + properties: + group_id: + type: string + x-go-name: GroupID + x-go-type: gidx.PrefixedID + x-go-type-import: + path: go.infratographer.com/x/gidx + members: + type: array + items: + type: string + x-go-type: gidx.PrefixedID + x-go-type-import: + path: go.infratographer.com/x/gidx + pagination: + $ref: '#/components/schemas/Pagination' diff --git a/pkg/api/v1/paginate_group_members.go b/pkg/api/v1/paginate_group_members.go new file mode 100644 index 00000000..9a008988 --- /dev/null +++ b/pkg/api/v1/paginate_group_members.go @@ -0,0 +1,44 @@ +package v1 + +import "go.infratographer.com/identity-api/internal/crdbx" + +var _ crdbx.Paginator = ListGroupMembersParams{} + +// GetCursor implements crdbx.Paginator returning the cursor. +func (p ListGroupMembersParams) GetCursor() *crdbx.Cursor { + return p.Cursor +} + +// GetLimit implements crdbx.Paginator returning requested limit. +func (p ListGroupMembersParams) GetLimit() int { + if p.Limit == nil { + return 0 + } + + return *p.Limit +} + +// GetOnlyFields implements crdbx.Paginator setting the only permitted field to `id`. +func (p ListGroupMembersParams) GetOnlyFields() []string { + return []string{"subject_id"} +} + +// SetPagination sets the pagination on the provided collection. +func (p ListGroupMembersParams) SetPagination(collection *GroupMemberCollection) error { + collection.Pagination.Limit = crdbx.Limit(p.GetLimit()) + + if count := len(collection.Members); count != 0 && count == collection.Pagination.Limit { + last := collection.Members[count-1] + + cursor, err := crdbx.NewCursor( + "subject_id", last.String(), + ) + if err != nil { + return err + } + + collection.Pagination.Next = cursor + } + + return nil +} diff --git a/pkg/api/v1/types.gen.go b/pkg/api/v1/types.gen.go index 244e8333..9c20cee5 100644 --- a/pkg/api/v1/types.gen.go +++ b/pkg/api/v1/types.gen.go @@ -11,13 +11,24 @@ import ( "net/url" "path" "strings" - "time" "github.com/getkin/kin-openapi/openapi3" "go.infratographer.com/identity-api/internal/crdbx" "go.infratographer.com/x/gidx" ) +// AddGroupMembers defines model for AddGroupMembers. +type AddGroupMembers struct { + // MemberIds IDs of the members to add to the group + MemberIds []gidx.PrefixedID `json:"member_ids"` +} + +// AddGroupMembersResponse defines model for AddGroupMembersResponse. +type AddGroupMembersResponse struct { + // Ok true if the members were added successfully + Ok bool `json:"ok"` +} + // CreateGroup defines model for CreateGroup. type CreateGroup struct { // Description a description for the group @@ -59,32 +70,17 @@ type DeleteResponse struct { // Group defines model for Group. type Group struct { - // CreatedAt time the group was created - CreatedAt *time.Time `json:"created_at,omitempty"` - - // CreatedBy ID of the user who created the group - CreatedBy *gidx.PrefixedID `json:"created_by,omitempty"` - // Description a description for the group Description *string `json:"description,omitempty"` // Id ID of the group ID gidx.PrefixedID `json:"id"` - // Members IDs of the members of the group - Members []gidx.PrefixedID `json:"members"` - // Name a name for the group Name string `json:"name"` // OwnerId ID of the owner of the group OwnerID *gidx.PrefixedID `json:"owner_id,omitempty"` - - // UpdatedAt time the group was last updated - UpdatedAt *time.Time `json:"updated_at,omitempty"` - - // UpdatedBy ID of the user who last updated the group - UpdatedBy *gidx.PrefixedID `json:"updated_by,omitempty"` } // Issuer defines model for Issuer. @@ -194,6 +190,15 @@ type GroupCollection struct { Pagination Pagination `json:"pagination"` } +// GroupMemberCollection defines model for GroupMemberCollection. +type GroupMemberCollection struct { + GroupID gidx.PrefixedID `json:"group_id"` + Members []gidx.PrefixedID `json:"members"` + + // Pagination collection response pagination + Pagination Pagination `json:"pagination"` +} + // IssuerCollection defines model for IssuerCollection. type IssuerCollection struct { Issuers []Issuer `json:"issuers"` @@ -217,6 +222,15 @@ type UserCollection struct { Users []User `json:"users"` } +// ListGroupMembersParams defines parameters for ListGroupMembers. +type ListGroupMembersParams struct { + // Cursor the cursor to the results to return + Cursor *PageCursor `form:"cursor,omitempty" json:"cursor,omitempty" query:"cursor"` + + // Limit limits the response collections + Limit *PageLimit `form:"limit,omitempty" json:"limit,omitempty" query:"limit"` +} + // GetIssuerUsersParams defines parameters for GetIssuerUsers. type GetIssuerUsersParams struct { // Cursor the cursor to the results to return @@ -256,6 +270,9 @@ type ListOwnerIssuersParams struct { // UpdateGroupJSONRequestBody defines body for UpdateGroup for application/json ContentType. type UpdateGroupJSONRequestBody = UpdateGroup +// AddGroupMembersJSONRequestBody defines body for AddGroupMembers for application/json ContentType. +type AddGroupMembersJSONRequestBody = AddGroupMembers + // UpdateIssuerJSONRequestBody defines body for UpdateIssuer for application/json ContentType. type UpdateIssuerJSONRequestBody = IssuerUpdate @@ -271,43 +288,45 @@ type CreateIssuerJSONRequestBody = CreateIssuer // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "HwAA//+Uzk9GJDwAAA==", } // GetSwagger returns the content of the embedded swagger specification file From acbd9238c623ebc46b938617a90ee9976d598d2d Mon Sep 17 00:00:00 2001 From: Bailin He Date: Thu, 19 Sep 2024 17:16:33 +0000 Subject: [PATCH 02/11] remove members Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 38 +++++++++ internal/api/httpsrv/server.gen.go | 75 +++++++++++++++++ internal/storage/groups.go | 4 +- internal/types/groups.go | 2 +- openapi-v1.yaml | 29 +++++++ pkg/api/v1/types.gen.go | 82 ++++++++++--------- 6 files changed, 188 insertions(+), 42 deletions(-) diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index e4db74cf..914cab1c 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -80,3 +80,41 @@ func (h *apiHandler) ListGroupMembers(ctx context.Context, req ListGroupMembersR return ListGroupMembers200JSONResponse{GroupMemberCollectionJSONResponse(collection)}, nil } + +// RemoveGroupMember removes a member from a group +func (h *apiHandler) RemoveGroupMember(ctx context.Context, req RemoveGroupMemberRequestObject) (RemoveGroupMemberResponseObject, error) { + gid := req.GroupID + sid := req.SubjectID + + if _, err := gidx.Parse(string(gid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid group id: %s", err.Error()), + ) + + return nil, err + } + + if _, err := gidx.Parse(string(sid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid member id: %s", err.Error()), + ) + + return nil, err + } + + if err := permissions.CheckAccess(ctx, gid, actionGroupUpdate); err != nil { + return nil, permissionsError(err) + } + + if err := h.engine.RemoveMember(ctx, gid, sid); err != nil { + if errors.Is(err, types.ErrNotFound) { + err = echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return nil, err + } + + return RemoveGroupMember200JSONResponse{true}, nil +} diff --git a/internal/api/httpsrv/server.gen.go b/internal/api/httpsrv/server.gen.go index b8e310e6..45f85e69 100644 --- a/internal/api/httpsrv/server.gen.go +++ b/internal/api/httpsrv/server.gen.go @@ -39,6 +39,9 @@ type ServerInterface interface { // Adds a member to a Group // (POST /api/v1/groups/{groupID}/members) AddGroupMembers(ctx echo.Context, groupID GroupID) error + // Removes a member from a Group + // (DELETE /api/v1/groups/{groupID}/members/{subjectID}) + RemoveGroupMember(ctx echo.Context, groupID GroupID, subjectID SubjectID) error // Deletes an issuer with the given ID. // (DELETE /api/v1/issuers/{id}) DeleteIssuer(ctx echo.Context, id gidx.PrefixedID) error @@ -207,6 +210,30 @@ func (w *ServerInterfaceWrapper) AddGroupMembers(ctx echo.Context) error { return err } +// RemoveGroupMember converts echo context to params. +func (w *ServerInterfaceWrapper) RemoveGroupMember(ctx echo.Context) error { + var err error + // ------------- Path parameter "groupID" ------------- + var groupID GroupID + + err = runtime.BindStyledParameterWithOptions("simple", "groupID", ctx.Param("groupID"), &groupID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter groupID: %s", err)) + } + + // ------------- Path parameter "subjectID" ------------- + var subjectID SubjectID + + err = runtime.BindStyledParameterWithOptions("simple", "subjectID", ctx.Param("subjectID"), &subjectID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter subjectID: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.RemoveGroupMember(ctx, groupID, subjectID) + return err +} + // DeleteIssuer converts echo context to params. func (w *ServerInterfaceWrapper) DeleteIssuer(ctx echo.Context) error { var err error @@ -482,6 +509,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PATCH(baseURL+"/api/v1/groups/:groupID", wrapper.UpdateGroup) router.GET(baseURL+"/api/v1/groups/:groupID/members", wrapper.ListGroupMembers) router.POST(baseURL+"/api/v1/groups/:groupID/members", wrapper.AddGroupMembers) + router.DELETE(baseURL+"/api/v1/groups/:groupID/members/:subjectID", wrapper.RemoveGroupMember) router.DELETE(baseURL+"/api/v1/issuers/:id", wrapper.DeleteIssuer) router.GET(baseURL+"/api/v1/issuers/:id", wrapper.GetIssuerByID) router.PATCH(baseURL+"/api/v1/issuers/:id", wrapper.UpdateIssuer) @@ -655,6 +683,24 @@ func (response AddGroupMembers200JSONResponse) VisitAddGroupMembersResponse(w ht return json.NewEncoder(w).Encode(response) } +type RemoveGroupMemberRequestObject struct { + GroupID GroupID `json:"groupID"` + SubjectID SubjectID `json:"subjectID"` +} + +type RemoveGroupMemberResponseObject interface { + VisitRemoveGroupMemberResponse(w http.ResponseWriter) error +} + +type RemoveGroupMember200JSONResponse DeleteResponse + +func (response RemoveGroupMember200JSONResponse) VisitRemoveGroupMemberResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + type DeleteIssuerRequestObject struct { Id gidx.PrefixedID `json:"id"` } @@ -875,6 +921,9 @@ type StrictServerInterface interface { // Adds a member to a Group // (POST /api/v1/groups/{groupID}/members) AddGroupMembers(ctx context.Context, request AddGroupMembersRequestObject) (AddGroupMembersResponseObject, error) + // Removes a member from a Group + // (DELETE /api/v1/groups/{groupID}/members/{subjectID}) + RemoveGroupMember(ctx context.Context, request RemoveGroupMemberRequestObject) (RemoveGroupMemberResponseObject, error) // Deletes an issuer with the given ID. // (DELETE /api/v1/issuers/{id}) DeleteIssuer(ctx context.Context, request DeleteIssuerRequestObject) (DeleteIssuerResponseObject, error) @@ -1110,6 +1159,32 @@ func (sh *strictHandler) AddGroupMembers(ctx echo.Context, groupID GroupID) erro return nil } +// RemoveGroupMember operation middleware +func (sh *strictHandler) RemoveGroupMember(ctx echo.Context, groupID GroupID, subjectID SubjectID) error { + var request RemoveGroupMemberRequestObject + + request.GroupID = groupID + request.SubjectID = subjectID + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.RemoveGroupMember(ctx.Request().Context(), request.(RemoveGroupMemberRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RemoveGroupMember") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(RemoveGroupMemberResponseObject); ok { + return validResponse.VisitRemoveGroupMemberResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // DeleteIssuer operation middleware func (sh *strictHandler) DeleteIssuer(ctx echo.Context, id gidx.PrefixedID) error { var request DeleteIssuerRequestObject diff --git a/internal/storage/groups.go b/internal/storage/groups.go index df9eeca4..485ed9fb 100644 --- a/internal/storage/groups.go +++ b/internal/storage/groups.go @@ -303,7 +303,7 @@ func (gs *groupService) ListMembers(ctx context.Context, groupID gidx.PrefixedID return members, nil } -func (gs *groupService) RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subjectID gidx.PrefixedID) error { +func (gs *groupService) RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subject gidx.PrefixedID) error { tx, err := getContextTx(ctx) if err != nil { return err @@ -318,7 +318,7 @@ func (gs *groupService) RemoveMember(ctx context.Context, groupID gidx.PrefixedI groupMemberCols.GroupID, groupMemberCols.SubjectID, ) - _, err = tx.ExecContext(ctx, q, groupID, subjectID) + _, err = tx.ExecContext(ctx, q, groupID, subject) return err } diff --git a/internal/types/groups.go b/internal/types/groups.go index 6af3d31b..bd7e56a8 100644 --- a/internal/types/groups.go +++ b/internal/types/groups.go @@ -51,7 +51,7 @@ type GroupService interface { AddMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) - // RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subjectID gidx.PrefixedID) error + RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subject gidx.PrefixedID) error // ReplaceMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error } diff --git a/openapi-v1.yaml b/openapi-v1.yaml index e91b4bed..ca2502fe 100644 --- a/openapi-v1.yaml +++ b/openapi-v1.yaml @@ -371,6 +371,24 @@ paths: schema: $ref: '#/components/schemas/AddGroupMembersResponse' + /api/v1/groups/{groupID}/members/{subjectID}: + delete: + tags: + - Groups + summary: Removes a member from a Group + description: Removes a member from a group by their ID. + operationId: removeGroupMember + parameters: + - $ref: '#/components/parameters/groupID' + - $ref: '#/components/parameters/subjectID' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteResponse' + components: schemas: DeleteResponse: @@ -630,6 +648,17 @@ components: x-go-type: gidx.PrefixedID x-go-type-import: path: go.infratographer.com/x/gidx + subjectID: + description: id of a subject + in: path + name: subjectID + x-go-name: SubjectID + required: true + schema: + type: string + x-go-type: gidx.PrefixedID + x-go-type-import: + path: go.infratographer.com/x/gidx pageCursor: description: the cursor to the results to return in: query diff --git a/pkg/api/v1/types.gen.go b/pkg/api/v1/types.gen.go index 9c20cee5..7afb5c52 100644 --- a/pkg/api/v1/types.gen.go +++ b/pkg/api/v1/types.gen.go @@ -182,6 +182,9 @@ type PageCursor = crdbx.Cursor // PageLimit defines model for pageLimit. type PageLimit = int +// SubjectID defines model for subjectID. +type SubjectID = gidx.PrefixedID + // GroupCollection defines model for GroupCollection. type GroupCollection struct { Groups []Group `json:"groups"` @@ -288,45 +291,46 @@ type CreateIssuerJSONRequestBody = CreateIssuer // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "bOJ37sw+fKTxBjX1tJxbjRZwdnBrKWtOHM2XvuW3R/W0WFtPO3TuqbLzuwW9A+acqNTX1N2nGKur7Rp/", - "vTCulYUZTb1llZTd+jaqsNVYpYcqgPmwr2oQX8mwnUvYOuS18N995GzN+185bD5H7VVRUmrer/OBABlW", - "92LG3fFSbpPtaH23eM/Ki859JI8nGWC0FzkTNzY8iqs5pJTho7sAXYSNK16DQwS9ttXTPhVjXl1q3jOI", - "/RfmPEhzUk9HDOL2gLkFeG8q4y/n7GWJxnjHzVyqkbHJSYZ+v7br37RYN9YxfCqOMsHvqNRNmd6ktXPO", - "4te6KP9iobEPzCvHx2fPmgbsYrPBUs+v67vi3j5ON2WIJIm7fm2MjwxaXdXC/UCu372W31bGOnw2bt4q", - "rbqezfjapn6+3WCkgvxFXe2tDUZqRWzSoPX8qXF73JsntcHYO0SNe90/hKP0ruD75otW6IHE2KrqnZf4", - "zP0pRT0v81pkPq0qIPZD5LFmsf02SvxG9tqwxDfFa/io/3EnIUMFqC6EN+m16xNujwXYfd5Ij23v9u90", - "3K9JNnViuyV7eu2e9WrHUhEScYbqwNY6tpcmBo192P6PFtXnrWJmHY2ytysvTclNNq5KluZ/9JK4uCr+", - "HwAA//+Uzk9GJDwAAA==", + "H4sIAAAAAAAC/+xb3W/bOBL/VwjePdwBipXsPl3e2mQReK+99uoWXbQbFLQ0ttlIpJakkvgC/e8Hfuib", + "kh3HDpzuPiWWqeHMb76H9AOOeJpxBkxJfP6AMyJICgqE+bQUPM+ml/rfGGQkaKYoZ/gc0xjxBSLILMAB", + "pvphRtQKB5iRFPB59W6ABfyRUwExPlcihwDLaAUp0UTVOtNLpRKULXGA70+W/MQ9XNL4fvJewILeQ2zo", + "VN+e0DTjQll+1Uov5hPKFoIovhQkW4GYRDwN70NNBBeFe9dxduU4KwJMpcxBjEjIkF3il5HGRyjetJSp", + "CDC/Y6PiIQGS5yICZFb6pSyJHJ+o7xxnRYAzsoSLXEgu+sKqFaDIfIcUR/qTAJknSuqPAlQuWCn5HzmI", + "dS26fQtvK2kk4vn95KJ86dFi0hiYomp9QjIaUqZAMJKEhqqTnZOMnkQ8hiWwE7hXgpwosjTOalmveC4c", + "KG9oSlUfk0Q/liUYGWcSUMSTBCK9QA7gYd7ywaGZXYLA2zJpCWkeZT7/DpEaM1K3xG+d9fvHZ5+zijf9", + "TYmzAcIEoYsKcP0o4kwBM5uRLEtoRPQ34Xdpv66FyQTPQCgKdZA2/1EFqfnn7wIW+Bz/LayDe2hfl6HZ", + "WOvJSU+EIGvnQZSRkpkxEu/rlVauEvWvJTMtatfVXtzqsdBvtVVNGsanle7oFIEF6i2kcxD7g+sbjQet", + "opMlDmkrqRGrrbvns9SDGEApUlADvR9jQCXlInApbi/mYNP79u5jtz6Y/5TsPBmzklAR4HevcrW6SCgw", + "tRfIIkNqe8ga+x8Mt5KnJ+NmmEUXjlwR4E9yT5a2m5wBzuVj7FOz20e5g5Yl+WSsLBmTxO3umrlXcdwI", + "2bKPg/XjbzSW/Yw/vZSasK5KnLvrEo3EcVm4VQ3HLuFyEywNzq6LoCvJB5e++xLxG0/NKXJAtC3KHQjQ", + "wkCMZB5FIOUiTxJdXjm25pwnQPrWzW8MQxcCiAKbwHtMtLZ/6Gmu8RktdCHcALONYVGWVn0i+vmmtzus", + "G1I18y58emIKoem3lGQZZbZSJHFM9cYked9a2WO2zeTFL28Q3GcCpNR1LHIkkeI3wJDZxtgUVysQ7jPu", + "2X6Av9/dyG+5oH0Yfv387xn69GHaE71dP+hletUgnK/QKk8JOxFAYjJPoI1u1Xb25PUy9enDtPPqBL3N", + "pUIpUdHKPP5dJ5ffsZUZ3ZJEGyhDlEU81Qj9+vmj3CCTkcenYMtVA7Va483o31M7yWMKLPKh477RLQpR", + "SK2oRDbIo4gwpDkAqYYjgSfR7KIGu+X2Vn4JCSgYDhXO7z1sJHdkLZEOG5PNAaEkc10WyIeNB7Za7obq", + "MlL7X+sMJS43x+anRB036vg2zqlZ8xi231Wjj1Heu0VcXLanji2jpxcT+sYxHAhMj1f3XxF2ywjbNKdO", + "mA261lMb2qcsJgr+yrQv2Q7azdtj0ueVIEwZWcs18lG50hcDbGv00+TUtUfIePlhov5lIz3xhdvQpyMJ", + "kQA1wGyD15ldtymRN32tQlc71ftW99beq9EVVbPURmsVdLSW+Cey2nLMV7qviusKpE8cBxjuSZolgM/P", + "ToPuDNaMYEWsc82ZBhju1ehMvNxJL9R8t+hj8vm///ry22o1/+21/DI7W31hH5KInp2Sq+R/bz4nN0Mm", + "8Cwj8Y72LLLXniBjo+Gxt05u3NDnEFJCkz7ZX/TjMjHrbnzb4q12Zb3ffhyZ+sraeiOblUaZ9Z1jDWP6", + "H43oBtllPh/jyc3nK71swZV7xR84NAR202ujTMoWvL//DKJcULVGH02mnIG4pRGgf8w+zv6J3hJGlpDq", + "kPXq/RRRiQgz/2keU/2lziCzjzMUcbagy1yYICNN00CVcdmBDdqkcYBvQUjL0unkdHJmqugMGMkoPsc/", + "T04nP5sZkVoZxYbaA2/PQjdqCx/sP9PLwoqoGx8zDcnA8jSNTSDXz5tZLGgdMn/1aydqZBjPoU+59d7O", + "fIriunNA89Pp6aOGfWNDuU5X6Bmtzap5EGos07aUpsScmFkaxhyaM0qtdnO29rVZKthCcGkTY1shV6D+", + "5NpozaN3UcUVKIm0b4vU7I/InOeq1kxddkyG1VMElUfZw67wwV2Z6PhTtzByZuBOReZrNL3U2/jc7srl", + "mY6KfeDUS8Ly5sbLcQlUClpifWWPDxtO0CmPtQY3QHgFypB5vTaWfYQYuqPcfZqwRXLihzLTHY6nIzK1", + "VQfPAHGWrG3ZQ1jcKqIiwtAcUG7ei/vIN4u1pwFvhoSvebzeG+ZN3jr1p454xXGqu1bRoKeMxKOwcU49", + "7E7Nk476ctiQd72hUrXOiXZWdLBxaeNu0Jar7aWZIef1EajWhf4LCx73a4E1EsEyLj2Yv4pjrU9LxJyS", + "jQPePZc7Nsfq8vfMzjV02LeTu3l08xS/Cx+qS06jtcEHSPktNHZeCJ427UKtgAqvddhXGwgc0h/rK1tH", + "X2IMQbqNOt0VjPCBxlu0SNNyDDlaj9s5vKWsDcvRPPTt1CNqj8TG9sihc0eVHccu6S0wZ/WlvqbuesxY", + "m2TX+Mu/ca0sQb1wlZTDl11UYYvrSg9VPvJhX5WUvgpwN5ewZeVz4b//RNg6vnnmLPgUtVc1Zql5v84H", + "AmRYXXMad8dPcpfihdZ34o+sWuxcL/N4kgFGe5EzcWPDo7iaM2cZPriL+0XYuLE3OBPSa1sjisdizKvL", + "+EcGsf/+owdpTuphl0Hc3hdoAd4bsvmrc3v3pTGtcyO06gTA5CRDv1+M9S/ObJrSGT4VR5ngt1TqHltv", + "0to5Z/Fz/cDjYKGxD8wzx8cnjw4H7GK7OWHPr+ur/962XPfYiCSJu01vjI8MWl3Vkf9Art/9lUVbGZvw", + "2boXr7TqWi3ja9v6+W5zrgryg7raS5tz1YrYpkHr+VPjxwDePKkNxl4Ja1zT/yEcpfeLCt+42Ao9kBhb", + "Vb3zEp+5P6ao52Vei8yrVQXEfog81iy2X0aJ38heW5b4pngNH/QfN7waKkB1IbxNr11fWPBYgN3nhfTY", + "9qcaez290SSbOrHdkr2M4J71asdSERJxhurA1rqFIU0MGnux/buZ6vVWMbOJRtnblXfg5DYbVyVL83d7", + "EhfXxf8DAAD//1t0f8zcPgAA", } // GetSwagger returns the content of the embedded swagger specification file From 2a9bb0ced2323b2067819de5d86bfd158f916c54 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Thu, 19 Sep 2024 19:17:10 +0000 Subject: [PATCH 03/11] replace members Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 31 ++++++- internal/api/httpsrv/server.gen.go | 72 ++++++++++++++++ internal/storage/groups.go | 23 +++++ internal/types/groups.go | 2 +- openapi-v1.yaml | 22 +++++ pkg/api/v1/types.gen.go | 85 ++++++++++--------- 6 files changed, 192 insertions(+), 43 deletions(-) diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index 914cab1c..5849329a 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -31,7 +31,7 @@ func (h *apiHandler) AddGroupMembers(ctx context.Context, req AddGroupMembersReq return nil, permissionsError(err) } - if err := h.engine.AddMembers(ctx, gid, reqbody.MemberIds...); err != nil { + if err := h.engine.AddMembers(ctx, gid, reqbody.MemberIDs...); err != nil { if errors.Is(err, types.ErrNotFound) { err = echo.NewHTTPError(http.StatusNotFound, err.Error()) } @@ -118,3 +118,32 @@ func (h *apiHandler) RemoveGroupMember(ctx context.Context, req RemoveGroupMembe return RemoveGroupMember200JSONResponse{true}, nil } + +// ReplaceGroupMembers replaces the members of a group +func (h *apiHandler) ReplaceGroupMembers(ctx context.Context, req ReplaceGroupMembersRequestObject) (ReplaceGroupMembersResponseObject, error) { + gid := req.GroupID + reqbody := req.Body + + if _, err := gidx.Parse(string(gid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid group id: %s", err.Error()), + ) + + return nil, err + } + + if err := permissions.CheckAccess(ctx, gid, actionGroupUpdate); err != nil { + return nil, permissionsError(err) + } + + if err := h.engine.ReplaceMembers(ctx, gid, reqbody.MemberIDs...); err != nil { + if errors.Is(err, types.ErrNotFound) { + err = echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return nil, err + } + + return ReplaceGroupMembers200JSONResponse{true}, nil +} diff --git a/internal/api/httpsrv/server.gen.go b/internal/api/httpsrv/server.gen.go index 45f85e69..454beec9 100644 --- a/internal/api/httpsrv/server.gen.go +++ b/internal/api/httpsrv/server.gen.go @@ -39,6 +39,9 @@ type ServerInterface interface { // Adds a member to a Group // (POST /api/v1/groups/{groupID}/members) AddGroupMembers(ctx echo.Context, groupID GroupID) error + // Replaces members of a Group + // (PUT /api/v1/groups/{groupID}/members) + ReplaceGroupMembers(ctx echo.Context, groupID GroupID) error // Removes a member from a Group // (DELETE /api/v1/groups/{groupID}/members/{subjectID}) RemoveGroupMember(ctx echo.Context, groupID GroupID, subjectID SubjectID) error @@ -210,6 +213,22 @@ func (w *ServerInterfaceWrapper) AddGroupMembers(ctx echo.Context) error { return err } +// ReplaceGroupMembers converts echo context to params. +func (w *ServerInterfaceWrapper) ReplaceGroupMembers(ctx echo.Context) error { + var err error + // ------------- Path parameter "groupID" ------------- + var groupID GroupID + + err = runtime.BindStyledParameterWithOptions("simple", "groupID", ctx.Param("groupID"), &groupID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter groupID: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ReplaceGroupMembers(ctx, groupID) + return err +} + // RemoveGroupMember converts echo context to params. func (w *ServerInterfaceWrapper) RemoveGroupMember(ctx echo.Context) error { var err error @@ -509,6 +528,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PATCH(baseURL+"/api/v1/groups/:groupID", wrapper.UpdateGroup) router.GET(baseURL+"/api/v1/groups/:groupID/members", wrapper.ListGroupMembers) router.POST(baseURL+"/api/v1/groups/:groupID/members", wrapper.AddGroupMembers) + router.PUT(baseURL+"/api/v1/groups/:groupID/members", wrapper.ReplaceGroupMembers) router.DELETE(baseURL+"/api/v1/groups/:groupID/members/:subjectID", wrapper.RemoveGroupMember) router.DELETE(baseURL+"/api/v1/issuers/:id", wrapper.DeleteIssuer) router.GET(baseURL+"/api/v1/issuers/:id", wrapper.GetIssuerByID) @@ -683,6 +703,24 @@ func (response AddGroupMembers200JSONResponse) VisitAddGroupMembersResponse(w ht return json.NewEncoder(w).Encode(response) } +type ReplaceGroupMembersRequestObject struct { + GroupID GroupID `json:"groupID"` + Body *ReplaceGroupMembersJSONRequestBody +} + +type ReplaceGroupMembersResponseObject interface { + VisitReplaceGroupMembersResponse(w http.ResponseWriter) error +} + +type ReplaceGroupMembers200JSONResponse AddGroupMembersResponse + +func (response ReplaceGroupMembers200JSONResponse) VisitReplaceGroupMembersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + type RemoveGroupMemberRequestObject struct { GroupID GroupID `json:"groupID"` SubjectID SubjectID `json:"subjectID"` @@ -921,6 +959,9 @@ type StrictServerInterface interface { // Adds a member to a Group // (POST /api/v1/groups/{groupID}/members) AddGroupMembers(ctx context.Context, request AddGroupMembersRequestObject) (AddGroupMembersResponseObject, error) + // Replaces members of a Group + // (PUT /api/v1/groups/{groupID}/members) + ReplaceGroupMembers(ctx context.Context, request ReplaceGroupMembersRequestObject) (ReplaceGroupMembersResponseObject, error) // Removes a member from a Group // (DELETE /api/v1/groups/{groupID}/members/{subjectID}) RemoveGroupMember(ctx context.Context, request RemoveGroupMemberRequestObject) (RemoveGroupMemberResponseObject, error) @@ -1159,6 +1200,37 @@ func (sh *strictHandler) AddGroupMembers(ctx echo.Context, groupID GroupID) erro return nil } +// ReplaceGroupMembers operation middleware +func (sh *strictHandler) ReplaceGroupMembers(ctx echo.Context, groupID GroupID) error { + var request ReplaceGroupMembersRequestObject + + request.GroupID = groupID + + var body ReplaceGroupMembersJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ReplaceGroupMembers(ctx.Request().Context(), request.(ReplaceGroupMembersRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ReplaceGroupMembers") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ReplaceGroupMembersResponseObject); ok { + return validResponse.VisitReplaceGroupMembersResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // RemoveGroupMember operation middleware func (sh *strictHandler) RemoveGroupMember(ctx echo.Context, groupID GroupID, subjectID SubjectID) error { var request RemoveGroupMemberRequestObject diff --git a/internal/storage/groups.go b/internal/storage/groups.go index 485ed9fb..c89bfccb 100644 --- a/internal/storage/groups.go +++ b/internal/storage/groups.go @@ -322,3 +322,26 @@ func (gs *groupService) RemoveMember(ctx context.Context, groupID gidx.PrefixedI return err } + +func (gs *groupService) ReplaceMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error { + tx, err := getContextTx(ctx) + if err != nil { + return err + } + + if _, err := gs.fetchGroupByID(ctx, groupID); err != nil { + return err + } + + delq := fmt.Sprintf( + "DELETE FROM group_members WHERE %s = $1", + groupMemberCols.GroupID, + ) + + _, err = tx.ExecContext(ctx, delq, groupID) + if err != nil { + return err + } + + return gs.AddMembers(ctx, groupID, subjects...) +} diff --git a/internal/types/groups.go b/internal/types/groups.go index bd7e56a8..fbe5b670 100644 --- a/internal/types/groups.go +++ b/internal/types/groups.go @@ -52,7 +52,7 @@ type GroupService interface { AddMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subject gidx.PrefixedID) error - // ReplaceMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error + ReplaceMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error } // Groups represents a list of groups diff --git a/openapi-v1.yaml b/openapi-v1.yaml index ca2502fe..033c54fc 100644 --- a/openapi-v1.yaml +++ b/openapi-v1.yaml @@ -349,6 +349,27 @@ paths: responses: '200': $ref: '#/components/responses/GroupMemberCollection' + put: + tags: + - Groups + summary: Replaces members of a Group + description: Replaces the members of a group by ID. + operationId: replaceGroupMembers + parameters: + - $ref: '#/components/parameters/groupID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddGroupMembers' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AddGroupMembersResponse' post: tags: - Groups @@ -601,6 +622,7 @@ components: properties: member_ids: type: array + x-go-name: MemberIDs items: type: string x-go-type: gidx.PrefixedID diff --git a/pkg/api/v1/types.gen.go b/pkg/api/v1/types.gen.go index 7afb5c52..01f61caf 100644 --- a/pkg/api/v1/types.gen.go +++ b/pkg/api/v1/types.gen.go @@ -20,7 +20,7 @@ import ( // AddGroupMembers defines model for AddGroupMembers. type AddGroupMembers struct { // MemberIds IDs of the members to add to the group - MemberIds []gidx.PrefixedID `json:"member_ids"` + MemberIDs []gidx.PrefixedID `json:"member_ids"` } // AddGroupMembersResponse defines model for AddGroupMembersResponse. @@ -276,6 +276,9 @@ type UpdateGroupJSONRequestBody = UpdateGroup // AddGroupMembersJSONRequestBody defines body for AddGroupMembers for application/json ContentType. type AddGroupMembersJSONRequestBody = AddGroupMembers +// ReplaceGroupMembersJSONRequestBody defines body for ReplaceGroupMembers for application/json ContentType. +type ReplaceGroupMembersJSONRequestBody = AddGroupMembers + // UpdateIssuerJSONRequestBody defines body for UpdateIssuer for application/json ContentType. type UpdateIssuerJSONRequestBody = IssuerUpdate @@ -291,46 +294,46 @@ type CreateIssuerJSONRequestBody = CreateIssuer // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "EhfXxf8DAAD//1t0f8zcPgAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file From b202451f7d63d3570037a92fa2cbad9a2c1daee4 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Thu, 19 Sep 2024 19:36:59 +0000 Subject: [PATCH 04/11] update group memebers actions Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index 5849329a..fea40c07 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -13,6 +13,13 @@ import ( "go.infratographer.com/x/gidx" ) +const ( + actionGroupMembersList = "iam_group_members_list" + actionGroupMembersAdd = "iam_group_members_add" + actionGroupMembersPut = "iam_group_members_put" + actionGroupMembersRemove = "iam_group_members_remove" +) + // AddGroupMembers creates a group func (h *apiHandler) AddGroupMembers(ctx context.Context, req AddGroupMembersRequestObject) (AddGroupMembersResponseObject, error) { reqbody := req.Body @@ -27,7 +34,7 @@ func (h *apiHandler) AddGroupMembers(ctx context.Context, req AddGroupMembersReq return nil, err } - if err := permissions.CheckAccess(ctx, gid, actionGroupUpdate); err != nil { + if err := permissions.CheckAccess(ctx, gid, actionGroupMembersAdd); err != nil { return nil, permissionsError(err) } @@ -55,7 +62,7 @@ func (h *apiHandler) ListGroupMembers(ctx context.Context, req ListGroupMembersR return nil, err } - if err := permissions.CheckAccess(ctx, gid, actionGroupGet); err != nil { + if err := permissions.CheckAccess(ctx, gid, actionGroupMembersList); err != nil { return nil, permissionsError(err) } @@ -104,7 +111,7 @@ func (h *apiHandler) RemoveGroupMember(ctx context.Context, req RemoveGroupMembe return nil, err } - if err := permissions.CheckAccess(ctx, gid, actionGroupUpdate); err != nil { + if err := permissions.CheckAccess(ctx, gid, actionGroupMembersRemove); err != nil { return nil, permissionsError(err) } @@ -133,7 +140,7 @@ func (h *apiHandler) ReplaceGroupMembers(ctx context.Context, req ReplaceGroupMe return nil, err } - if err := permissions.CheckAccess(ctx, gid, actionGroupUpdate); err != nil { + if err := permissions.CheckAccess(ctx, gid, actionGroupMembersPut); err != nil { return nil, permissionsError(err) } From 6b67e105e4da9f8bac3516a3d59a179eb6c243d7 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Thu, 19 Sep 2024 21:15:15 +0000 Subject: [PATCH 05/11] list user groups Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group.go | 2 +- internal/api/httpsrv/handler_group_members.go | 81 +++++++++++++++++ internal/api/httpsrv/server.gen.go | 83 +++++++++++++++++ internal/storage/groups.go | 72 +++++++++++++-- internal/types/groups.go | 4 +- openapi-v1.yaml | 20 +++++ pkg/api/v1/paginate_user_groups.go | 40 +++++++++ pkg/api/v1/types.gen.go | 90 ++++++++++--------- 8 files changed, 344 insertions(+), 48 deletions(-) create mode 100644 pkg/api/v1/paginate_user_groups.go diff --git a/internal/api/httpsrv/handler_group.go b/internal/api/httpsrv/handler_group.go index 54668060..9d4a05cc 100644 --- a/internal/api/httpsrv/handler_group.go +++ b/internal/api/httpsrv/handler_group.go @@ -137,7 +137,7 @@ func (h *apiHandler) ListGroups(ctx context.Context, req ListGroupsRequestObject return nil, permissionsError(err) } - groups, err := h.engine.ListGroups(ctx, ownerID, req.Params) + groups, err := h.engine.ListGroupsByOwner(ctx, ownerID, req.Params) if err != nil { return nil, err } diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index fea40c07..a03c8276 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -34,6 +34,17 @@ func (h *apiHandler) AddGroupMembers(ctx context.Context, req AddGroupMembersReq return nil, err } + for _, mid := range reqbody.MemberIDs { + if _, err := gidx.Parse(string(mid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid member id %s: %s", mid, err.Error()), + ) + + return nil, err + } + } + if err := permissions.CheckAccess(ctx, gid, actionGroupMembersAdd); err != nil { return nil, permissionsError(err) } @@ -102,6 +113,13 @@ func (h *apiHandler) RemoveGroupMember(ctx context.Context, req RemoveGroupMembe return nil, err } + if _, err := gidx.Parse(string(sid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid member id: %s", err.Error()), + ) + } + if _, err := gidx.Parse(string(sid)); err != nil { err = echo.NewHTTPError( http.StatusBadRequest, @@ -140,6 +158,17 @@ func (h *apiHandler) ReplaceGroupMembers(ctx context.Context, req ReplaceGroupMe return nil, err } + for _, mid := range reqbody.MemberIDs { + if _, err := gidx.Parse(string(mid)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid member id %s: %s", mid, err.Error()), + ) + + return nil, err + } + } + if err := permissions.CheckAccess(ctx, gid, actionGroupMembersPut); err != nil { return nil, permissionsError(err) } @@ -154,3 +183,55 @@ func (h *apiHandler) ReplaceGroupMembers(ctx context.Context, req ReplaceGroupMe return ReplaceGroupMembers200JSONResponse{true}, nil } + +func (h *apiHandler) ListUserGroups(ctx context.Context, req ListUserGroupsRequestObject) (ListUserGroupsResponseObject, error) { + subject := req.UserID + + if _, err := gidx.Parse(string(subject)); err != nil { + err = echo.NewHTTPError( + http.StatusBadRequest, + fmt.Sprintf("invalid subject id: %s", err.Error()), + ) + + return nil, err + } + + // Find the owner the user's issuer is on to check permissions. + ownerID, err := h.engine.LookupUserOwnerID(ctx, subject) + switch err { + case nil: + case types.ErrUserInfoNotFound: + return nil, echo.NewHTTPError(http.StatusNotFound, err.Error()) + default: + return nil, err + } + + if err := permissions.CheckAccess(ctx, ownerID, actionUserGet); err != nil { + return nil, permissionsError(err) + } + + groups, err := h.engine.ListGroupsBySubject(ctx, subject, req.Params) + if err != nil { + if errors.Is(err, types.ErrNotFound) { + err = echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return nil, err + } + + resp, err := groups.ToV1Groups() + if err != nil { + return nil, err + } + + collection := v1.GroupCollection{ + Groups: resp, + Pagination: v1.Pagination{}, + } + + if err := req.Params.SetPagination(&collection); err != nil { + return nil, err + } + + return ListUserGroups200JSONResponse{GroupCollectionJSONResponse(collection)}, nil +} diff --git a/internal/api/httpsrv/server.gen.go b/internal/api/httpsrv/server.gen.go index 454beec9..263b0e03 100644 --- a/internal/api/httpsrv/server.gen.go +++ b/internal/api/httpsrv/server.gen.go @@ -78,6 +78,9 @@ type ServerInterface interface { // Gets information about a User. // (GET /api/v1/users/{userID}) GetUserByID(ctx echo.Context, userID gidx.PrefixedID) error + // Lists groups by user id + // (GET /api/v1/users/{userID}/groups) + ListUserGroups(ctx echo.Context, userID gidx.PrefixedID, params ListUserGroupsParams) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -493,6 +496,38 @@ func (w *ServerInterfaceWrapper) GetUserByID(ctx echo.Context) error { return err } +// ListUserGroups converts echo context to params. +func (w *ServerInterfaceWrapper) ListUserGroups(ctx echo.Context) error { + var err error + // ------------- Path parameter "userID" ------------- + var userID gidx.PrefixedID + + err = runtime.BindStyledParameterWithOptions("simple", "userID", ctx.Param("userID"), &userID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter userID: %s", err)) + } + + // Parameter object where we will unmarshal all parameters from the context + var params ListUserGroupsParams + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", ctx.QueryParams(), ¶ms.Cursor) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter cursor: %s", err)) + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ListUserGroups(ctx, userID, params) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -541,6 +576,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/api/v1/owners/:ownerID/issuers", wrapper.ListOwnerIssuers) router.POST(baseURL+"/api/v1/owners/:ownerID/issuers", wrapper.CreateIssuer) router.GET(baseURL+"/api/v1/users/:userID", wrapper.GetUserByID) + router.GET(baseURL+"/api/v1/users/:userID/groups", wrapper.ListUserGroups) } @@ -936,6 +972,24 @@ func (response GetUserByID200JSONResponse) VisitGetUserByIDResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type ListUserGroupsRequestObject struct { + UserID gidx.PrefixedID `json:"userID"` + Params ListUserGroupsParams +} + +type ListUserGroupsResponseObject interface { + VisitListUserGroupsResponse(w http.ResponseWriter) error +} + +type ListUserGroups200JSONResponse struct{ GroupCollectionJSONResponse } + +func (response ListUserGroups200JSONResponse) VisitListUserGroupsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // Deletes an OAuth Client @@ -998,6 +1052,9 @@ type StrictServerInterface interface { // Gets information about a User. // (GET /api/v1/users/{userID}) GetUserByID(ctx context.Context, request GetUserByIDRequestObject) (GetUserByIDResponseObject, error) + // Lists groups by user id + // (GET /api/v1/users/{userID}/groups) + ListUserGroups(ctx context.Context, request ListUserGroupsRequestObject) (ListUserGroupsResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -1559,3 +1616,29 @@ func (sh *strictHandler) GetUserByID(ctx echo.Context, userID gidx.PrefixedID) e } return nil } + +// ListUserGroups operation middleware +func (sh *strictHandler) ListUserGroups(ctx echo.Context, userID gidx.PrefixedID, params ListUserGroupsParams) error { + var request ListUserGroupsRequestObject + + request.UserID = userID + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.ListUserGroups(ctx.Request().Context(), request.(ListUserGroupsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListUserGroups") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(ListUserGroupsResponseObject); ok { + return validResponse.VisitListUserGroupsResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/internal/storage/groups.go b/internal/storage/groups.go index c89bfccb..cb5f4b52 100644 --- a/internal/storage/groups.go +++ b/internal/storage/groups.go @@ -112,19 +112,20 @@ func (gs *groupService) fetchGroupByID(ctx context.Context, id gidx.PrefixedID) groupColsStr, groupCols.ID, ) - var row *sql.Row + var ex func(ctx context.Context, query string, args ...any) *sql.Row tx, err := getContextTx(ctx) - switch err { case nil: - row = tx.QueryRowContext(ctx, q, id) + ex = tx.QueryRowContext case ErrorMissingContextTx: - row = gs.db.QueryRowContext(ctx, q, id) + ex = gs.db.QueryRowContext default: return nil, err } + row := ex(ctx, q, id) + return gs.scanGroup(row) } @@ -146,7 +147,7 @@ func (gs *groupService) scanGroup(row *sql.Row) (*types.Group, error) { return &g, nil } -func (gs *groupService) ListGroups(ctx context.Context, ownerID gidx.PrefixedID, pagination crdbx.Paginator) (types.Groups, error) { +func (gs *groupService) ListGroupsByOwner(ctx context.Context, ownerID gidx.PrefixedID, pagination crdbx.Paginator) (types.Groups, error) { paginate := crdbx.Paginate(pagination, crdbx.ContextAsOfSystemTime(ctx, "-1m")) q := fmt.Sprintf( @@ -271,7 +272,7 @@ func (gs *groupService) AddMembers(ctx context.Context, groupID gidx.PrefixedID, } func (gs *groupService) ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) { - paginate := crdbx.Paginate(pagination, crdbx.ContextAsOfSystemTime(ctx, nil)) + paginate := crdbx.Paginate(pagination, crdbx.ContextAsOfSystemTime(ctx, "-1m")) q := fmt.Sprintf( "SELECT %s FROM group_members %s WHERE %s = $1 %s %s %s", @@ -345,3 +346,62 @@ func (gs *groupService) ReplaceMembers(ctx context.Context, groupID gidx.Prefixe return gs.AddMembers(ctx, groupID, subjects...) } + +func (gs *groupService) ListGroupsBySubject(ctx context.Context, subject gidx.PrefixedID, pagination crdbx.Paginator) (types.Groups, error) { + paginate := crdbx.Paginate(pagination, crdbx.ContextAsOfSystemTime(ctx, "-1m")) + + const ( + membersTable = "group_members" + groupsTable = "groups" + ) + + q := fmt.Sprintf( + `SELECT %s FROM %s LEFT JOIN %s ON %s %s WHERE %s = $1 %s %s %s`, + // SELECT + strings.Join([]string{ + fmt.Sprintf("DISTINCT(%s.%s)", membersTable, groupMemberCols.GroupID), + fmt.Sprintf("%s.%s", groupsTable, groupCols.Name), + fmt.Sprintf("%s.%s", groupsTable, groupCols.Description), + fmt.Sprintf("%s.%s", groupsTable, groupCols.OwnerID), + }, ", "), + // FROM + membersTable, + // LEFT JOIN + groupsTable, + // ON + fmt.Sprintf( + "%s.%s = %s.%s", + groupsTable, groupCols.ID, + membersTable, groupMemberCols.GroupID, + ), + // as of system time + paginate.AsOfSystemTime(), + // WHERE + fmt.Sprintf("%s.%s", membersTable, groupMemberCols.SubjectID), + // Pagination + paginate.AndWhere(2), //nolint:gomnd + paginate.OrderClause(), + paginate.LimitClause(), + ) + + rows, err := gs.db.QueryContext(ctx, q, subject) + if err != nil { + return nil, err + } + + defer rows.Close() + + var groups types.Groups + + for rows.Next() { + g := &types.Group{} + + if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.OwnerID); err != nil { + return nil, err + } + + groups = append(groups, g) + } + + return groups, nil +} diff --git a/internal/types/groups.go b/internal/types/groups.go index fbe5b670..6fa5840b 100644 --- a/internal/types/groups.go +++ b/internal/types/groups.go @@ -45,10 +45,12 @@ type GroupUpdate struct { type GroupService interface { CreateGroup(ctx context.Context, group Group) (*Group, error) GetGroupByID(ctx context.Context, id gidx.PrefixedID) (*Group, error) - ListGroups(ctx context.Context, ownerID gidx.PrefixedID, pagination crdbx.Paginator) (Groups, error) UpdateGroup(ctx context.Context, id gidx.PrefixedID, update GroupUpdate) (*Group, error) DeleteGroup(ctx context.Context, id gidx.PrefixedID) error + ListGroupsByOwner(ctx context.Context, ownerID gidx.PrefixedID, pagination crdbx.Paginator) (Groups, error) + ListGroupsBySubject(ctx context.Context, subject gidx.PrefixedID, pagination crdbx.Paginator) (Groups, error) + AddMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subject gidx.PrefixedID) error diff --git a/openapi-v1.yaml b/openapi-v1.yaml index 033c54fc..5dbd7931 100644 --- a/openapi-v1.yaml +++ b/openapi-v1.yaml @@ -282,6 +282,26 @@ paths: schema: $ref: '#/components/schemas/User' + /api/v1/users/{userID}/groups: + get: + tags: + - Users + summary: Lists groups by user id + operationId: ListUserGroups + parameters: + - in: path + name: userID + required: true + description: User ID + schema: + type: string + x-go-type: gidx.PrefixedID + - $ref: '#/components/parameters/pageCursor' + - $ref: '#/components/parameters/pageLimit' + responses: + '200': + $ref: '#/components/responses/GroupCollection' + /api/v1/groups/{groupID}: delete: tags: diff --git a/pkg/api/v1/paginate_user_groups.go b/pkg/api/v1/paginate_user_groups.go new file mode 100644 index 00000000..ba034bd6 --- /dev/null +++ b/pkg/api/v1/paginate_user_groups.go @@ -0,0 +1,40 @@ +package v1 + +import "go.infratographer.com/identity-api/internal/crdbx" + +var _ crdbx.Paginator = ListUserGroupsParams{} + +// GetCursor implements crdbx.Paginator returning the cursor. +func (p ListUserGroupsParams) GetCursor() *crdbx.Cursor { + return p.Cursor +} + +// GetLimit implements crdbx.Paginator returning requested limit. +func (p ListUserGroupsParams) GetLimit() int { + if p.Limit == nil { + return 0 + } + + return *p.Limit +} + +// GetOnlyFields implements crdbx.Paginator setting the only permitted field to `id`. +func (p ListUserGroupsParams) GetOnlyFields() []string { + return []string{"group_id"} +} + +// SetPagination sets the pagination on the provided collection. +func (p ListUserGroupsParams) SetPagination(collection *GroupCollection) error { + collection.Pagination.Limit = crdbx.Limit(p.GetLimit()) + + if count := len(collection.Groups); count != 0 && count == collection.Pagination.Limit { + cursor, err := crdbx.NewCursor("group_id", collection.Groups[count-1].ID.String()) + if err != nil { + return err + } + + collection.Pagination.Next = cursor + } + + return nil +} diff --git a/pkg/api/v1/types.gen.go b/pkg/api/v1/types.gen.go index 01f61caf..29029d41 100644 --- a/pkg/api/v1/types.gen.go +++ b/pkg/api/v1/types.gen.go @@ -270,6 +270,15 @@ type ListOwnerIssuersParams struct { Limit *PageLimit `form:"limit,omitempty" json:"limit,omitempty" query:"limit"` } +// ListUserGroupsParams defines parameters for ListUserGroups. +type ListUserGroupsParams struct { + // Cursor the cursor to the results to return + Cursor *PageCursor `form:"cursor,omitempty" json:"cursor,omitempty" query:"cursor"` + + // Limit limits the response collections + Limit *PageLimit `form:"limit,omitempty" json:"limit,omitempty" query:"limit"` +} + // UpdateGroupJSONRequestBody defines body for UpdateGroup for application/json ContentType. type UpdateGroupJSONRequestBody = UpdateGroup @@ -294,46 +303,47 @@ type CreateIssuerJSONRequestBody = CreateIssuer // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "alT/0lZ4+U3+3wAAAP//CyiSho5CAAA=", } // GetSwagger returns the content of the embedded swagger specification file From 3d54f38114e497df4d13c8c4a29155e635dcf39c Mon Sep 17 00:00:00 2001 From: Bailin He Date: Thu, 19 Sep 2024 21:41:56 +0000 Subject: [PATCH 06/11] fix things Signed-off-by: Bailin He --- internal/storage/groups.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/storage/groups.go b/internal/storage/groups.go index cb5f4b52..04cb2460 100644 --- a/internal/storage/groups.go +++ b/internal/storage/groups.go @@ -252,9 +252,14 @@ func (gs *groupService) AddMembers(ctx context.Context, groupID gidx.PrefixedID, } vals := make([]string, 0, len(subjects)) + params := make([]any, 0, len(subjects)+1) + params = append(params, groupID) - for _, subj := range subjects { - vals = append(vals, fmt.Sprintf("('%s', '%s')", groupID, subj)) + const placeholderOffset = 2 + + for i, subj := range subjects { + vals = append(vals, fmt.Sprintf("($1, $%d)", i+placeholderOffset)) + params = append(params, subj) } q := fmt.Sprintf( @@ -263,7 +268,7 @@ func (gs *groupService) AddMembers(ctx context.Context, groupID gidx.PrefixedID, strings.Join(vals, ", "), ) - _, err = tx.ExecContext(ctx, q) + _, err = tx.ExecContext(ctx, q, params...) if err != nil { fmt.Println(err.Error()) } @@ -384,7 +389,7 @@ func (gs *groupService) ListGroupsBySubject(ctx context.Context, subject gidx.Pr paginate.LimitClause(), ) - rows, err := gs.db.QueryContext(ctx, q, subject) + rows, err := gs.db.QueryContext(ctx, q, paginate.Values(subject)...) if err != nil { return nil, err } From c2dc3c426236b9ddaa29be224abfb006621e8035 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Mon, 23 Sep 2024 14:42:20 +0000 Subject: [PATCH 07/11] some fixes * fix some names * return only group IDs on list-user-groups Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 13 +-- internal/api/httpsrv/server.gen.go | 13 ++- internal/types/groups.go | 11 +++ openapi-v1.yaml | 27 +++++- pkg/api/v1/paginate_group_members.go | 4 +- pkg/api/v1/paginate_user_groups.go | 6 +- pkg/api/v1/types.gen.go | 94 ++++++++++--------- 7 files changed, 106 insertions(+), 62 deletions(-) diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index a03c8276..73c7d7f0 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -87,7 +87,7 @@ func (h *apiHandler) ListGroupMembers(ctx context.Context, req ListGroupMembersR } collection := v1.GroupMemberCollection{ - Members: members, + MemberIDs: members, GroupID: gid, Pagination: v1.Pagination{}, } @@ -219,13 +219,10 @@ func (h *apiHandler) ListUserGroups(ctx context.Context, req ListUserGroupsReque return nil, err } - resp, err := groups.ToV1Groups() - if err != nil { - return nil, err - } + resp := groups.ToIDs() - collection := v1.GroupCollection{ - Groups: resp, + collection := v1.GroupIDCollection{ + GroupIDs: resp, Pagination: v1.Pagination{}, } @@ -233,5 +230,5 @@ func (h *apiHandler) ListUserGroups(ctx context.Context, req ListUserGroupsReque return nil, err } - return ListUserGroups200JSONResponse{GroupCollectionJSONResponse(collection)}, nil + return ListUserGroups200JSONResponse{GroupIDCollectionJSONResponse(collection)}, nil } diff --git a/internal/api/httpsrv/server.gen.go b/internal/api/httpsrv/server.gen.go index 263b0e03..3fc8bc42 100644 --- a/internal/api/httpsrv/server.gen.go +++ b/internal/api/httpsrv/server.gen.go @@ -587,9 +587,16 @@ type GroupCollectionJSONResponse struct { Pagination Pagination `json:"pagination"` } +type GroupIDCollectionJSONResponse struct { + GroupIDs []gidx.PrefixedID `json:"group_ids"` + + // Pagination collection response pagination + Pagination Pagination `json:"pagination"` +} + type GroupMemberCollectionJSONResponse struct { - GroupID gidx.PrefixedID `json:"group_id"` - Members []gidx.PrefixedID `json:"members"` + GroupID gidx.PrefixedID `json:"group_id"` + MemberIDs []gidx.PrefixedID `json:"member_ids"` // Pagination collection response pagination Pagination Pagination `json:"pagination"` @@ -981,7 +988,7 @@ type ListUserGroupsResponseObject interface { VisitListUserGroupsResponse(w http.ResponseWriter) error } -type ListUserGroups200JSONResponse struct{ GroupCollectionJSONResponse } +type ListUserGroups200JSONResponse struct{ GroupIDCollectionJSONResponse } func (response ListUserGroups200JSONResponse) VisitListUserGroupsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") diff --git a/internal/types/groups.go b/internal/types/groups.go index 6fa5840b..e0b080e3 100644 --- a/internal/types/groups.go +++ b/internal/types/groups.go @@ -75,3 +75,14 @@ func (g Groups) ToV1Groups() ([]v1.Group, error) { return out, nil } + +// ToIDs converts a list of groups to a list of group IDs. +func (g Groups) ToIDs() []gidx.PrefixedID { + out := make([]gidx.PrefixedID, len(g)) + + for i, group := range g { + out[i] = group.ID + } + + return out +} diff --git a/openapi-v1.yaml b/openapi-v1.yaml index 5dbd7931..34eea901 100644 --- a/openapi-v1.yaml +++ b/openapi-v1.yaml @@ -287,6 +287,7 @@ paths: tags: - Users summary: Lists groups by user id + description: Lists groups by user id. operationId: ListUserGroups parameters: - in: path @@ -300,7 +301,7 @@ paths: - $ref: '#/components/parameters/pageLimit' responses: '200': - $ref: '#/components/responses/GroupCollection' + $ref: '#/components/responses/GroupIDCollection' /api/v1/groups/{groupID}: delete: @@ -788,6 +789,25 @@ components: $ref: '#/components/schemas/Group' pagination: $ref: '#/components/schemas/Pagination' + GroupIDCollection: + description: a collection of group ids + content: + application/json: + schema: + type: object + required: + - group_ids + - pagination + properties: + group_ids: + type: array + x-go-name: GroupIDs + items: + type: string + x-go-type: gidx.PrefixedID + x-go-type-import: + pagination: + $ref: '#/components/schemas/Pagination' GroupMemberCollection: description: a collection of group members content: @@ -795,7 +815,7 @@ components: schema: type: object required: - - members + - member_ids - group_id - pagination properties: @@ -805,8 +825,9 @@ components: x-go-type: gidx.PrefixedID x-go-type-import: path: go.infratographer.com/x/gidx - members: + member_ids: type: array + x-go-name: MemberIDs items: type: string x-go-type: gidx.PrefixedID diff --git a/pkg/api/v1/paginate_group_members.go b/pkg/api/v1/paginate_group_members.go index 9a008988..5b2cb70d 100644 --- a/pkg/api/v1/paginate_group_members.go +++ b/pkg/api/v1/paginate_group_members.go @@ -27,8 +27,8 @@ func (p ListGroupMembersParams) GetOnlyFields() []string { func (p ListGroupMembersParams) SetPagination(collection *GroupMemberCollection) error { collection.Pagination.Limit = crdbx.Limit(p.GetLimit()) - if count := len(collection.Members); count != 0 && count == collection.Pagination.Limit { - last := collection.Members[count-1] + if count := len(collection.MemberIDs); count != 0 && count == collection.Pagination.Limit { + last := collection.MemberIDs[count-1] cursor, err := crdbx.NewCursor( "subject_id", last.String(), diff --git a/pkg/api/v1/paginate_user_groups.go b/pkg/api/v1/paginate_user_groups.go index ba034bd6..692f60c3 100644 --- a/pkg/api/v1/paginate_user_groups.go +++ b/pkg/api/v1/paginate_user_groups.go @@ -24,11 +24,11 @@ func (p ListUserGroupsParams) GetOnlyFields() []string { } // SetPagination sets the pagination on the provided collection. -func (p ListUserGroupsParams) SetPagination(collection *GroupCollection) error { +func (p ListUserGroupsParams) SetPagination(collection *GroupIDCollection) error { collection.Pagination.Limit = crdbx.Limit(p.GetLimit()) - if count := len(collection.Groups); count != 0 && count == collection.Pagination.Limit { - cursor, err := crdbx.NewCursor("group_id", collection.Groups[count-1].ID.String()) + if count := len(collection.GroupIDs); count != 0 && count == collection.Pagination.Limit { + cursor, err := crdbx.NewCursor("group_id", collection.GroupIDs[count-1].String()) if err != nil { return err } diff --git a/pkg/api/v1/types.gen.go b/pkg/api/v1/types.gen.go index 29029d41..4364699d 100644 --- a/pkg/api/v1/types.gen.go +++ b/pkg/api/v1/types.gen.go @@ -193,10 +193,18 @@ type GroupCollection struct { Pagination Pagination `json:"pagination"` } +// GroupIDCollection defines model for GroupIDCollection. +type GroupIDCollection struct { + GroupIDs []gidx.PrefixedID `json:"group_ids"` + + // Pagination collection response pagination + Pagination Pagination `json:"pagination"` +} + // GroupMemberCollection defines model for GroupMemberCollection. type GroupMemberCollection struct { - GroupID gidx.PrefixedID `json:"group_id"` - Members []gidx.PrefixedID `json:"members"` + GroupID gidx.PrefixedID `json:"group_id"` + MemberIDs []gidx.PrefixedID `json:"member_ids"` // Pagination collection response pagination Pagination Pagination `json:"pagination"` @@ -303,47 +311,47 @@ type CreateIssuerJSONRequestBody = CreateIssuer // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "alT/0lZ4+U3+3wAAAP//CyiSho5CAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file From e8f31c2a8f825bc0d69d82b35971f775e152ae60 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Mon, 23 Sep 2024 18:11:44 +0000 Subject: [PATCH 08/11] add user-info-groups endpoint Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 2 +- internal/types/groups.go | 14 +++- internal/userinfo/handler.go | 64 ++++++++++++++++++- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index 73c7d7f0..c0fbf62c 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -219,7 +219,7 @@ func (h *apiHandler) ListUserGroups(ctx context.Context, req ListUserGroupsReque return nil, err } - resp := groups.ToIDs() + resp := groups.ToPrefixedIDs() collection := v1.GroupIDCollection{ GroupIDs: resp, diff --git a/internal/types/groups.go b/internal/types/groups.go index e0b080e3..dad65a91 100644 --- a/internal/types/groups.go +++ b/internal/types/groups.go @@ -43,17 +43,27 @@ type GroupUpdate struct { // GroupService represents a service for managing groups. type GroupService interface { + // CreateGroup creates a new group. CreateGroup(ctx context.Context, group Group) (*Group, error) + // GetGroupByID retrieves a group by its ID. GetGroupByID(ctx context.Context, id gidx.PrefixedID) (*Group, error) + // UpdateGroup updates a group. UpdateGroup(ctx context.Context, id gidx.PrefixedID, update GroupUpdate) (*Group, error) + // DeleteGroup deletes a group. DeleteGroup(ctx context.Context, id gidx.PrefixedID) error + // ListGroupsByOwner retrieves a list of groups owned by an OU. ListGroupsByOwner(ctx context.Context, ownerID gidx.PrefixedID, pagination crdbx.Paginator) (Groups, error) + // ListGroupsBySubject retrieves a list of groups that a subject is a member of. ListGroupsBySubject(ctx context.Context, subject gidx.PrefixedID, pagination crdbx.Paginator) (Groups, error) + // AddMembers adds subjects to a group. AddMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error + // ListMembers retrieves a list of subjects in a group. ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) + // RemoveMember removes a subject from a group. RemoveMember(ctx context.Context, groupID gidx.PrefixedID, subject gidx.PrefixedID) error + // ReplaceMembers replaces the members of a group with a new set of subjects. ReplaceMembers(ctx context.Context, groupID gidx.PrefixedID, subjects ...gidx.PrefixedID) error } @@ -76,8 +86,8 @@ func (g Groups) ToV1Groups() ([]v1.Group, error) { return out, nil } -// ToIDs converts a list of groups to a list of group IDs. -func (g Groups) ToIDs() []gidx.PrefixedID { +// ToPrefixedIDs converts a list of groups to a list of group IDs. +func (g Groups) ToPrefixedIDs() []gidx.PrefixedID { out := make([]gidx.PrefixedID, len(g)) for i, group := range g { diff --git a/internal/userinfo/handler.go b/internal/userinfo/handler.go index c0750757..f0cd144d 100644 --- a/internal/userinfo/handler.go +++ b/internal/userinfo/handler.go @@ -3,22 +3,32 @@ package userinfo import ( + "fmt" "net/http" + "strconv" "github.com/labstack/echo/v4" + "go.infratographer.com/identity-api/internal/crdbx" "go.infratographer.com/identity-api/internal/types" + v1 "go.infratographer.com/identity-api/pkg/api/v1" "go.infratographer.com/x/echojwtx" "go.infratographer.com/x/gidx" ) +// Store is an interface providing userinfo and group services +type Store interface { + types.UserInfoService + types.GroupService +} + // Handler provides the endpoint for /userinfo type Handler struct { - store types.UserInfoService + store Store } // NewHandler creates a UserInfo handler with the storage engine -func NewHandler(userInfoSvc types.UserInfoService) (*Handler, error) { +func NewHandler(userInfoSvc Store) (*Handler, error) { return &Handler{ store: userInfoSvc, }, nil @@ -42,7 +52,57 @@ func (h *Handler) handle(ctx echo.Context) error { return ctx.JSON(http.StatusOK, info) } +// ListUserGroups expects an authenticated request using a STS token and +// returns the groups the user is a member of. +func (h *Handler) listUserGroups(ctx echo.Context) error { + fullSubject := echojwtx.Actor(ctx) + + resourceID, err := gidx.Parse(fullSubject) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "invalid subject").SetInternal(err) + } + + cursor := ctx.QueryParam("cursor") + limit := ctx.QueryParam("limit") + + pagination := v1.ListUserGroupsParams{} + + if cursor != "" { + c := crdbx.Cursor(cursor) + pagination.Cursor = &c + } + + if limit != "" { + limitInt, err := strconv.Atoi(limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid limit: %s", limit)) + } + + l := crdbx.Limit(limitInt) + pagination.Limit = &l + } + + groups, err := h.store.ListGroupsBySubject(ctx.Request().Context(), resourceID, pagination) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + + resp := groups.ToPrefixedIDs() + + collection := v1.GroupIDCollection{ + GroupIDs: resp, + Pagination: v1.Pagination{}, + } + + if err := pagination.SetPagination(&collection); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err) + } + + return ctx.JSON(http.StatusOK, collection) +} + // Routes registers the userinfo handler in a echo.Group func (h *Handler) Routes(rg *echo.Group) { rg.GET("userinfo", h.handle) + rg.GET("userinfo/groups", h.listUserGroups) } From 707656004270f340cd4787c1e3c2a9e69ba6fdb0 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Tue, 24 Sep 2024 15:50:53 +0000 Subject: [PATCH 09/11] add tests Signed-off-by: Bailin He --- .../api/httpsrv/handler_group_members_test.go | 511 ++++++++++++++++++ internal/storage/groups.go | 16 +- internal/types/errors.go | 3 + 3 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 internal/api/httpsrv/handler_group_members_test.go diff --git a/internal/api/httpsrv/handler_group_members_test.go b/internal/api/httpsrv/handler_group_members_test.go new file mode 100644 index 00000000..a42ef83b --- /dev/null +++ b/internal/api/httpsrv/handler_group_members_test.go @@ -0,0 +1,511 @@ +package httpsrv + +import ( + "context" + "net/http" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + pagination "go.infratographer.com/identity-api/internal/crdbx" + "go.infratographer.com/identity-api/internal/storage" + "go.infratographer.com/identity-api/internal/testingx" + "go.infratographer.com/identity-api/internal/types" + v1 "go.infratographer.com/identity-api/pkg/api/v1" + "go.infratographer.com/x/crdbx" + "go.infratographer.com/x/gidx" +) + +func TestGroupMembersAPIHandler(t *testing.T) { + t.Parallel() + + testServer, err := storage.InMemoryCRDB() + if !assert.NoError(t, err) { + assert.FailNow(t, "initialization failed") + } + + err = testServer.Start() + if !assert.NoError(t, err) { + assert.FailNow(t, "initialization failed") + } + + t.Cleanup(func() { + testServer.Stop() + }) + + ownerID := gidx.MustNewID("testten") + + config := crdbx.Config{ + URI: testServer.PGURL().String(), + } + + store, err := storage.NewEngine(config, storage.WithMigrations()) + if !assert.NoError(t, err) { + assert.FailNow(t, "initialization failed") + } + + setupFn := func(ctx context.Context) context.Context { + ctx, err := store.BeginContext(ctx) + if !assert.NoError(t, err) { + assert.FailNow(t, "setup failed") + } + + return ctx + } + + cleanupFn := func(ctx context.Context) { + err := store.RollbackContext(ctx) + assert.NoError(t, err) + } + + t.Run("ListGroupMembers", func(t *testing.T) { + t.Parallel() + + handler := apiHandler{engine: store} + + testGroup := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-list-group-members", + } + + someMembers := []gidx.PrefixedID{ + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + } + + withStoredGroupAndMembers(t, store, testGroup, someMembers...) + + tc := []testingx.TestCase[ListGroupMembersRequestObject, ListGroupMembersResponseObject]{ + { + Name: "Invalid group id", + Input: ListGroupMembersRequestObject{GroupID: "definitely not a valid group id"}, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[ListGroupMembersResponseObject]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Group not found", + Input: ListGroupMembersRequestObject{GroupID: gidx.MustNewID(types.IdentityGroupIDPrefix)}, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[ListGroupMembersResponseObject]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Success default pagination", + Input: ListGroupMembersRequestObject{GroupID: testGroup.ID}, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[ListGroupMembersResponseObject]) { + assert.Nil(t, res.Err) + assert.IsType(t, ListGroupMembers200JSONResponse{}, res.Success) + + members := res.Success.(ListGroupMembers200JSONResponse) + assert.Len(t, members.MemberIDs, len(someMembers)) + assert.NotNil(t, members.Pagination.Limit) + }, + }, + { + Name: "Success custom pagination", + Input: ListGroupMembersRequestObject{ + GroupID: testGroup.ID, + Params: v1.ListGroupMembersParams{ + Limit: ptr(1), + }, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[ListGroupMembersResponseObject]) { + assert.Nil(t, res.Err) + assert.IsType(t, ListGroupMembers200JSONResponse{}, res.Success) + + members := res.Success.(ListGroupMembers200JSONResponse) + assert.Len(t, members.MemberIDs, 1) + assert.Equal(t, members.Pagination.Limit, 1) + assert.NotNil(t, members.Pagination.Next) + }, + }, + } + + runFn := func(ctx context.Context, input ListGroupMembersRequestObject) testingx.TestResult[ListGroupMembersResponseObject] { + ctx = pagination.AsOfSystemTime(ctx, "") + resp, err := handler.ListGroupMembers(ctx, input) + + return testingx.TestResult[ListGroupMembersResponseObject]{Success: resp, Err: err} + } + + testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + }) + + t.Run("AddGroupMembers", func(t *testing.T) { + t.Parallel() + + handler := apiHandler{engine: store} + + testGroupWithNoMembers := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-add-group-members", + } + + testGroupWithSomeMembers := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-add-group-members-with-some-members", + } + + someMembers := []gidx.PrefixedID{ + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + } + + withStoredGroupAndMembers(t, store, testGroupWithNoMembers) + withStoredGroupAndMembers(t, store, testGroupWithSomeMembers, someMembers...) + + tc := []testingx.TestCase[AddGroupMembersRequestObject, []gidx.PrefixedID]{ + { + Name: "Invalid group id", + Input: AddGroupMembersRequestObject{GroupID: "definitely not a valid group id"}, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Group not found", + Input: AddGroupMembersRequestObject{ + GroupID: gidx.MustNewID(types.IdentityGroupIDPrefix), + Body: &v1.AddGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Invalid member id", + Input: AddGroupMembersRequestObject{ + GroupID: testGroupWithNoMembers.ID, + Body: &v1.AddGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{"definitely not a valid member id"}, + }, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Success", + Input: AddGroupMembersRequestObject{ + GroupID: testGroupWithNoMembers.ID, + Body: &v1.AddGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: setupFn, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Err) + assert.Len(t, res.Success, 1) + }, + }, + { + Name: "Success with adding existing members", + Input: AddGroupMembersRequestObject{ + GroupID: testGroupWithSomeMembers.ID, + Body: &v1.AddGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{someMembers[0]}, + }, + }, + SetupFn: setupFn, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Err) + assert.Len(t, res.Success, len(someMembers)) + }, + }, + } + + runFn := func(ctx context.Context, input AddGroupMembersRequestObject) testingx.TestResult[[]gidx.PrefixedID] { + _, err := handler.AddGroupMembers(ctx, input) + if err != nil { + return testingx.TestResult[[]gidx.PrefixedID]{Err: err} + } + + if err := store.CommitContext(ctx); err != nil { + return testingx.TestResult[[]gidx.PrefixedID]{Err: err} + } + + ctx = context.Background() + ctx = pagination.AsOfSystemTime(ctx, "") + p := v1.ListGroupMembersParams{} + mm, err := store.ListMembers(ctx, input.GroupID, p) + + return testingx.TestResult[[]gidx.PrefixedID]{Success: mm, Err: err} + } + + testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + }) + + t.Run("RemoveGroupMember", func(t *testing.T) { + t.Parallel() + + handler := apiHandler{engine: store} + + testGroup := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-remove-group-member", + } + + someMembers := []gidx.PrefixedID{ + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + } + + withStoredGroupAndMembers(t, store, testGroup, someMembers...) + + tc := []testingx.TestCase[RemoveGroupMemberRequestObject, []gidx.PrefixedID]{ + { + Name: "Invalid group id", + Input: RemoveGroupMemberRequestObject{ + GroupID: "definitely not a valid group id", + SubjectID: gidx.MustNewID(types.IdentityUserIDPrefix), + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Invalid member id", + Input: RemoveGroupMemberRequestObject{ + GroupID: testGroup.ID, + SubjectID: "definitely not a valid member id", + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Group not found", + Input: RemoveGroupMemberRequestObject{ + GroupID: gidx.MustNewID(types.IdentityGroupIDPrefix), + SubjectID: gidx.MustNewID(types.IdentityUserIDPrefix), + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Member not found", + Input: RemoveGroupMemberRequestObject{ + GroupID: testGroup.ID, + SubjectID: gidx.MustNewID(types.IdentityUserIDPrefix), + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Success", + Input: RemoveGroupMemberRequestObject{ + GroupID: testGroup.ID, + SubjectID: someMembers[0], + }, + SetupFn: setupFn, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Err) + assert.Len(t, res.Success, len(someMembers)-1) + }, + }, + } + + runFn := func(ctx context.Context, input RemoveGroupMemberRequestObject) testingx.TestResult[[]gidx.PrefixedID] { + _, err := handler.RemoveGroupMember(ctx, input) + if err != nil { + return testingx.TestResult[[]gidx.PrefixedID]{Err: err} + } + + if err := store.CommitContext(ctx); err != nil { + return testingx.TestResult[[]gidx.PrefixedID]{Err: err} + } + + ctx = context.Background() + ctx = pagination.AsOfSystemTime(ctx, "") + p := v1.ListGroupMembersParams{} + mm, err := store.ListMembers(ctx, input.GroupID, p) + + return testingx.TestResult[[]gidx.PrefixedID]{Success: mm, Err: err} + } + + testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + }) + + t.Run("PutGroupMembers", func(t *testing.T) { + t.Parallel() + + handler := apiHandler{engine: store} + + testGroup := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-put-group-members", + } + + someMembers := []gidx.PrefixedID{ + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + } + + withStoredGroupAndMembers(t, store, testGroup, someMembers...) + + tc := []testingx.TestCase[ReplaceGroupMembersRequestObject, []gidx.PrefixedID]{ + { + Name: "Invalid group id", + Input: ReplaceGroupMembersRequestObject{ + GroupID: "definitely not a valid group id", + Body: &v1.ReplaceGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Invalid member id", + Input: ReplaceGroupMembersRequestObject{ + GroupID: testGroup.ID, + Body: &v1.ReplaceGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{"definitely not a valid member id"}, + }, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Group not found", + Input: ReplaceGroupMembersRequestObject{ + GroupID: gidx.MustNewID(types.IdentityGroupIDPrefix), + Body: &v1.ReplaceGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Success) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + }, + }, + { + Name: "Success", + Input: ReplaceGroupMembersRequestObject{ + GroupID: testGroup.ID, + Body: &v1.ReplaceGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: setupFn, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Nil(t, res.Err) + assert.Len(t, res.Success, 1) + }, + }, + } + + runFn := func(ctx context.Context, input ReplaceGroupMembersRequestObject) testingx.TestResult[[]gidx.PrefixedID] { + _, err := handler.ReplaceGroupMembers(ctx, input) + if err != nil { + return testingx.TestResult[[]gidx.PrefixedID]{Err: err} + } + + if err := store.CommitContext(ctx); err != nil { + return testingx.TestResult[[]gidx.PrefixedID]{Err: err} + } + + ctx = context.Background() + ctx = pagination.AsOfSystemTime(ctx, "") + p := v1.ListGroupMembersParams{} + mm, err := store.ListMembers(ctx, input.GroupID, p) + + return testingx.TestResult[[]gidx.PrefixedID]{Success: mm, Err: err} + } + + testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + }) +} + +func withStoredGroupAndMembers(t *testing.T, s storage.Engine, group *types.Group, m ...gidx.PrefixedID) { + seedCtx, err := s.BeginContext(context.Background()) + if !assert.NoError(t, err) { + assert.FailNow(t, "failed to begin context") + } + + g, err := s.CreateGroup(seedCtx, *group) + if !assert.NoError(t, err) { + assert.FailNow(t, "failed to create group") + } + + *group = *g + + if err := s.AddMembers(seedCtx, group.ID, m...); !assert.NoError(t, err) { + assert.FailNow(t, "failed to add members") + } + + if err := s.CommitContext(seedCtx); !assert.NoError(t, err) { + assert.FailNow(t, "error committing seed groups") + } +} diff --git a/internal/storage/groups.go b/internal/storage/groups.go index 04cb2460..ad08bd14 100644 --- a/internal/storage/groups.go +++ b/internal/storage/groups.go @@ -279,6 +279,10 @@ func (gs *groupService) AddMembers(ctx context.Context, groupID gidx.PrefixedID, func (gs *groupService) ListMembers(ctx context.Context, groupID gidx.PrefixedID, pagination crdbx.Paginator) ([]gidx.PrefixedID, error) { paginate := crdbx.Paginate(pagination, crdbx.ContextAsOfSystemTime(ctx, "-1m")) + if _, err := gs.fetchGroupByID(ctx, groupID); err != nil { + return nil, err + } + q := fmt.Sprintf( "SELECT %s FROM group_members %s WHERE %s = $1 %s %s %s", groupMemberCols.SubjectID, paginate.AsOfSystemTime(), groupMemberCols.GroupID, @@ -324,7 +328,17 @@ func (gs *groupService) RemoveMember(ctx context.Context, groupID gidx.PrefixedI groupMemberCols.GroupID, groupMemberCols.SubjectID, ) - _, err = tx.ExecContext(ctx, q, groupID, subject) + res, err := tx.ExecContext(ctx, q, groupID, subject) + if err != nil { + return err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } else if rowsAffected == 0 { + return types.ErrGroupMemberNotFound + } return err } diff --git a/internal/types/errors.go b/internal/types/errors.go index 0685fb6f..ba800f58 100644 --- a/internal/types/errors.go +++ b/internal/types/errors.go @@ -36,6 +36,9 @@ var ( // ErrGroupNameEmpty is returned if the group name is empty. ErrGroupNameEmpty = fmt.Errorf("%w: group name is empty", ErrInvalidArgument) + + // ErrGroupMemberNotFound is returned if the group member doesn't exist. + ErrGroupMemberNotFound = fmt.Errorf("%w: group member not found", ErrNotFound) ) // ErrorInvalidTokenRequest represents an error where an access token request failed. From 95b81713e10c29d8d60900ee277b0d7f7e06da33 Mon Sep 17 00:00:00 2001 From: Bailin He <15058035+bailinhe@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:33:35 -0400 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Mike Mason Signed-off-by: Bailin He <15058035+bailinhe@users.noreply.github.com> --- pkg/api/v1/paginate_group_members.go | 2 +- pkg/api/v1/paginate_user_groups.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/v1/paginate_group_members.go b/pkg/api/v1/paginate_group_members.go index 5b2cb70d..5076f547 100644 --- a/pkg/api/v1/paginate_group_members.go +++ b/pkg/api/v1/paginate_group_members.go @@ -18,7 +18,7 @@ func (p ListGroupMembersParams) GetLimit() int { return *p.Limit } -// GetOnlyFields implements crdbx.Paginator setting the only permitted field to `id`. +// GetOnlyFields implements crdbx.Paginator setting the only permitted field to `subject_id`. func (p ListGroupMembersParams) GetOnlyFields() []string { return []string{"subject_id"} } diff --git a/pkg/api/v1/paginate_user_groups.go b/pkg/api/v1/paginate_user_groups.go index 692f60c3..cd5681c9 100644 --- a/pkg/api/v1/paginate_user_groups.go +++ b/pkg/api/v1/paginate_user_groups.go @@ -18,7 +18,7 @@ func (p ListUserGroupsParams) GetLimit() int { return *p.Limit } -// GetOnlyFields implements crdbx.Paginator setting the only permitted field to `id`. +// GetOnlyFields implements crdbx.Paginator setting the only permitted field to `group_id`. func (p ListUserGroupsParams) GetOnlyFields() []string { return []string{"group_id"} } From c7e9e1825f0ab8ee0f800e07ea2ed3cacb7aac50 Mon Sep 17 00:00:00 2001 From: Bailin He Date: Mon, 30 Sep 2024 16:39:11 +0000 Subject: [PATCH 11/11] use success instead of ok Signed-off-by: Bailin He --- internal/api/httpsrv/handler_group_members.go | 2 +- openapi-v1.yaml | 4 +- pkg/api/v1/types.gen.go | 86 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/internal/api/httpsrv/handler_group_members.go b/internal/api/httpsrv/handler_group_members.go index c0fbf62c..c46c0859 100644 --- a/internal/api/httpsrv/handler_group_members.go +++ b/internal/api/httpsrv/handler_group_members.go @@ -57,7 +57,7 @@ func (h *apiHandler) AddGroupMembers(ctx context.Context, req AddGroupMembersReq return nil, err } - return AddGroupMembers200JSONResponse{Ok: true}, nil + return AddGroupMembers200JSONResponse{Success: true}, nil } // ListGroupMembers lists the members of a group diff --git a/openapi-v1.yaml b/openapi-v1.yaml index 34eea901..bd1f33de 100644 --- a/openapi-v1.yaml +++ b/openapi-v1.yaml @@ -651,9 +651,9 @@ components: AddGroupMembersResponse: required: - - ok + - success properties: - ok: + success: type: boolean description: true if the members were added successfully diff --git a/pkg/api/v1/types.gen.go b/pkg/api/v1/types.gen.go index 4364699d..6ab5b0f3 100644 --- a/pkg/api/v1/types.gen.go +++ b/pkg/api/v1/types.gen.go @@ -25,8 +25,8 @@ type AddGroupMembers struct { // AddGroupMembersResponse defines model for AddGroupMembersResponse. type AddGroupMembersResponse struct { - // Ok true if the members were added successfully - Ok bool `json:"ok"` + // Success true if the members were added successfully + Success bool `json:"success"` } // CreateGroup defines model for CreateGroup. @@ -311,47 +311,47 @@ type CreateIssuerJSONRequestBody = CreateIssuer // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file