diff --git a/go.mod b/go.mod index 8b5deac8..634edfb3 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-jose/go-jose/v3 v3.0.1 github.com/google/uuid v1.6.0 github.com/spf13/pflag v1.0.5 - github.com/unikorn-cloud/core v0.1.18 + github.com/unikorn-cloud/core v0.1.19 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/go.sum b/go.sum index 933feedc..803d5b20 100644 --- a/go.sum +++ b/go.sum @@ -261,8 +261,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unikorn-cloud/core v0.1.18 h1:jc8Euf5mRGMJiSpZfnPLboJwG744AMsjW5r0bGn5Xd0= -github.com/unikorn-cloud/core v0.1.18/go.mod h1:5LzHGYsCfMxC9tv+QblOKH6CDYryX1umvaLrYFh0y6M= +github.com/unikorn-cloud/core v0.1.19 h1:nMXAnSEdE1q6rLqOt5fNvPSqTDx2fGk2kp0dHXqQDL0= +github.com/unikorn-cloud/core v0.1.19/go.mod h1:5LzHGYsCfMxC9tv+QblOKH6CDYryX1umvaLrYFh0y6M= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= diff --git a/openapi/server.spec.yaml b/openapi/server.spec.yaml index 7d568ed6..f2e1d027 100644 --- a/openapi/server.spec.yaml +++ b/openapi/server.spec.yaml @@ -216,9 +216,11 @@ paths: - oauth2Authentication: [] responses: '200': - description: A list of groups. + $ref: '#/components/responses/groupsResponse' '401': - description: Invalid credentials were provided. + $ref: '#/components/responses/unauthorizedResponse' + '403': + $ref: '#/components/responses/forbiddenResponse' post: description: |- Allows creation of a new group. @@ -230,11 +232,11 @@ paths: '201': description: Group successfully created and returned. '401': - description: Invalid credentials were provided. + $ref: '#/components/responses/unauthorizedResponse' '403': - description: The user is forbidden from creating groups. + $ref: '#/components/responses/forbiddenResponse' '409': - description: The group already exists. + $ref: '#/components/responses/conflictResponse' /api/v1/organizations/{organization}/groups/{groupid}: description: |- Allows management of organization groups. Groups provide an identity @@ -255,25 +257,25 @@ paths: '200': description: Group successfully updated and returned. '401': - description: Invalid credentials were provided. + $ref: '#/components/responses/unauthorizedResponse' '403': - description: The user is forbidden from creating groups. + $ref: '#/components/responses/forbiddenResponse' '404': - description: The requested group does not exist. + $ref: '#/components/responses/notFoundResponse' delete: description: |- Allows the deletion of an existing group. security: - oauth2Authentication: [] responses: - '204': + '200': description: Group successfully deleted. '401': - description: Invalid credentials were provided. + $ref: '#/components/responses/unauthorizedResponse' '403': - description: The user is forbidden from deleting groups. - '410': - description: The requested group does not exist. + $ref: '#/components/responses/forbiddenResponse' + '404': + $ref: '#/components/responses/notFoundResponse' components: parameters: organizationParameter: @@ -606,6 +608,41 @@ components: type: array items: $ref: '#/components/schemas/organization' + userList: + description: A list of users. + type: array + items: + description: A canonical user name (e.g email address). + type: string + roleList: + description: A list of roles. + type: array + items: + description: A role name. + type: string + group: + description: A group. + type: object + required: + - id + - name + - roles + properties: + id: + description: An immutable group ID. + type: string + name: + description: The group name. + type: string + users: + $ref: '#/components/schemas/userList' + roles: + $ref: '#/components/schemas/roleList' + groups: + description: A list of groups. + type: array + items: + $ref: '#/components/schemas/group' requestBodies: loginRequest: description: Information necessary to resolve a federated SSO provider. @@ -650,14 +687,14 @@ components: content: application/json: schema: - type: object + $ref: '#/components/schemas/group' updateGroupRequest: description: Body required to update a group. required: true content: application/json: schema: - type: object + $ref: '#/components/schemas/group' responses: badRequestResponse: description: |- @@ -800,6 +837,20 @@ components: - name: acme-corp domain: acme.corp providerName: google-identity + groupsResponse: + description: |- + A list of groups for the organization. + content: + application/json: + schema: + $ref: '#/components/schemas/groups' + example: + - id: 401cfc2c-2135-4619-a6ff-1ac247a3b2ad + name: The A-Team + users: + - face@a-team.com + roles: + - admin securitySchemes: oauth2Authentication: description: Operation requires OAuth2 bearer token authentication. diff --git a/org.yaml b/org.yaml new file mode 100644 index 00000000..9063896f --- /dev/null +++ b/org.yaml @@ -0,0 +1,15 @@ +apiVersion: identity.unikorn-cloud.org/v1alpha1 +kind: Organization +metadata: + name: nscale + namespace: unikorn-identity +spec: + domain: nscale.com + providerName: google-identity + groups: + - id: 8e4516ed-69f1-4509-889b-a1de21738294 + name: Platform Administrators + roles: + - superAdmin + users: + - simon.murray@nscale.com diff --git a/pkg/generated/client.go b/pkg/generated/client.go index e43093ba..6f24dc9d 100644 --- a/pkg/generated/client.go +++ b/pkg/generated/client.go @@ -1136,6 +1136,9 @@ func (r PutApiV1OrganizationsOrganizationResponse) StatusCode() int { type GetApiV1OrganizationsOrganizationGroupsResponse struct { Body []byte HTTPResponse *http.Response + JSON200 *Groups + JSON401 *Oauth2Error + JSON403 *Oauth2Error } // Status returns HTTPResponse.Status @@ -1157,6 +1160,9 @@ func (r GetApiV1OrganizationsOrganizationGroupsResponse) StatusCode() int { type PostApiV1OrganizationsOrganizationGroupsResponse struct { Body []byte HTTPResponse *http.Response + JSON401 *Oauth2Error + JSON403 *Oauth2Error + JSON409 *Oauth2Error } // Status returns HTTPResponse.Status @@ -1178,6 +1184,9 @@ func (r PostApiV1OrganizationsOrganizationGroupsResponse) StatusCode() int { type DeleteApiV1OrganizationsOrganizationGroupsGroupidResponse struct { Body []byte HTTPResponse *http.Response + JSON401 *Oauth2Error + JSON403 *Oauth2Error + JSON404 *Oauth2Error } // Status returns HTTPResponse.Status @@ -1199,6 +1208,9 @@ func (r DeleteApiV1OrganizationsOrganizationGroupsGroupidResponse) StatusCode() type PutApiV1OrganizationsOrganizationGroupsGroupidResponse struct { Body []byte HTTPResponse *http.Response + JSON401 *Oauth2Error + JSON403 *Oauth2Error + JSON404 *Oauth2Error } // Status returns HTTPResponse.Status @@ -1717,6 +1729,30 @@ func ParseGetApiV1OrganizationsOrganizationGroupsResponse(rsp *http.Response) (* HTTPResponse: rsp, } + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Groups + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + } + return response, nil } @@ -1733,6 +1769,30 @@ func ParsePostApiV1OrganizationsOrganizationGroupsResponse(rsp *http.Response) ( HTTPResponse: rsp, } + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + } + return response, nil } @@ -1749,6 +1809,30 @@ func ParseDeleteApiV1OrganizationsOrganizationGroupsGroupidResponse(rsp *http.Re HTTPResponse: rsp, } + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + return response, nil } @@ -1765,6 +1849,30 @@ func ParsePutApiV1OrganizationsOrganizationGroupsGroupidResponse(rsp *http.Respo HTTPResponse: rsp, } + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Oauth2Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + return response, nil } diff --git a/pkg/generated/schema.go b/pkg/generated/schema.go index 50b06478..1a52d916 100644 --- a/pkg/generated/schema.go +++ b/pkg/generated/schema.go @@ -18,134 +18,138 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "8/opW3EAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/generated/types.go b/pkg/generated/types.go index ed30bfa1..d7b42f6c 100644 --- a/pkg/generated/types.go +++ b/pkg/generated/types.go @@ -103,6 +103,24 @@ type CodeChallengeMethod string // GrantType Supported grant type. type GrantType string +// Group A group. +type Group struct { + // Id An immutable group ID. + Id string `json:"id"` + + // Name The group name. + Name string `json:"name"` + + // Roles A list of roles. + Roles RoleList `json:"roles"` + + // Users A list of users. + Users *UserList `json:"users,omitempty"` +} + +// Groups A list of groups. +type Groups = []Group + // JsonWebKey JSON web key. See the relevant JWKS documentation for further details. type JsonWebKey = map[string]interface{} @@ -216,6 +234,9 @@ type Organizations = []Organization // ResponseType Supported response types. type ResponseType string +// RoleList A list of roles. +type RoleList = []string + // Scope Supported scopes. type Scope string @@ -272,6 +293,9 @@ type TokenRequestOptions1 struct { GrantType *interface{} `json:"grant_type,omitempty"` } +// UserList A list of users. +type UserList = []string + // GroupidParameter defines model for groupidParameter. type GroupidParameter = string @@ -287,6 +311,9 @@ type ConflictResponse = Oauth2Error // ForbiddenResponse Generic error message. type ForbiddenResponse = Oauth2Error +// GroupsResponse A list of groups. +type GroupsResponse = Groups + // InternalServerErrorResponse Generic error message. type InternalServerErrorResponse = Oauth2Error @@ -294,6 +321,9 @@ type InternalServerErrorResponse = Oauth2Error // committee. Consult the relevant documentation for further details. type JwksResponse = JsonWebKeySet +// NotFoundResponse Generic error message. +type NotFoundResponse = Oauth2Error + // Oauth2ProvidersResponse A list of oauth2 providers. type Oauth2ProvidersResponse = Oauth2Providers @@ -312,24 +342,18 @@ type UnauthorizedResponse = Oauth2Error // UserinfoResponse defines model for userinfoResponse. type UserinfoResponse interface{} -// CreateGroupRequest defines model for createGroupRequest. -type CreateGroupRequest = map[string]interface{} +// CreateGroupRequest A group. +type CreateGroupRequest = Group // CreateOrganizationRequest An organization. type CreateOrganizationRequest = Organization -// UpdateGroupRequest defines model for updateGroupRequest. -type UpdateGroupRequest = map[string]interface{} +// UpdateGroupRequest A group. +type UpdateGroupRequest = Group // UpdateOrganizationRequest An organization. type UpdateOrganizationRequest = Organization -// PostApiV1OrganizationsOrganizationGroupsJSONBody defines parameters for PostApiV1OrganizationsOrganizationGroups. -type PostApiV1OrganizationsOrganizationGroupsJSONBody = map[string]interface{} - -// PutApiV1OrganizationsOrganizationGroupsGroupidJSONBody defines parameters for PutApiV1OrganizationsOrganizationGroupsGroupid. -type PutApiV1OrganizationsOrganizationGroupsGroupidJSONBody = map[string]interface{} - // PostApiV1OrganizationsJSONRequestBody defines body for PostApiV1Organizations for application/json ContentType. type PostApiV1OrganizationsJSONRequestBody = Organization @@ -337,10 +361,10 @@ type PostApiV1OrganizationsJSONRequestBody = Organization type PutApiV1OrganizationsOrganizationJSONRequestBody = Organization // PostApiV1OrganizationsOrganizationGroupsJSONRequestBody defines body for PostApiV1OrganizationsOrganizationGroups for application/json ContentType. -type PostApiV1OrganizationsOrganizationGroupsJSONRequestBody = PostApiV1OrganizationsOrganizationGroupsJSONBody +type PostApiV1OrganizationsOrganizationGroupsJSONRequestBody = Group // PutApiV1OrganizationsOrganizationGroupsGroupidJSONRequestBody defines body for PutApiV1OrganizationsOrganizationGroupsGroupid for application/json ContentType. -type PutApiV1OrganizationsOrganizationGroupsGroupidJSONRequestBody = PutApiV1OrganizationsOrganizationGroupsGroupidJSONBody +type PutApiV1OrganizationsOrganizationGroupsGroupidJSONRequestBody = Group // PostOauth2V2LoginFormdataRequestBody defines body for PostOauth2V2Login for application/x-www-form-urlencoded ContentType. type PostOauth2V2LoginFormdataRequestBody = LoginRequestOptions diff --git a/pkg/handler/groups/client.go b/pkg/handler/groups/client.go new file mode 100644 index 00000000..96e33142 --- /dev/null +++ b/pkg/handler/groups/client.go @@ -0,0 +1,89 @@ +/* +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package groups + +import ( + "context" + "slices" + "strings" + + "github.com/unikorn-cloud/core/pkg/authorization/roles" + unikornv1 "github.com/unikorn-cloud/identity/pkg/apis/unikorn/v1alpha1" + "github.com/unikorn-cloud/identity/pkg/generated" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Client struct { + client client.Client + namespace string +} + +func New(client client.Client, namespace string) *Client { + return &Client{ + client: client, + namespace: namespace, + } +} + +func convertRoleList(in []roles.Role) generated.RoleList { + out := make([]string, len(in)) + + for i, role := range in { + out[i] = string(role) + } + + return out +} + +func convert(in *unikornv1.OrganizationGroup) *generated.Group { + out := &generated.Group{ + Id: in.ID, + Name: in.Name, + Roles: convertRoleList(in.Roles), + } + + if len(in.Users) > 0 { + out.Users = &in.Users + } + + return out +} + +func convertList(in []unikornv1.OrganizationGroup) generated.Groups { + slices.SortStableFunc(in, func(a, b unikornv1.OrganizationGroup) int { + return strings.Compare(a.Name, b.Name) + }) + + out := make(generated.Groups, len(in)) + + for i := range in { + out[i] = *convert(&in[i]) + } + + return out +} + +func (c *Client) List(ctx context.Context, organizationName string) (generated.Groups, error) { + var organization unikornv1.Organization + + if err := c.client.Get(ctx, client.ObjectKey{Namespace: c.namespace, Name: organizationName}, &organization); err != nil { + return nil, err + } + + return convertList(organization.Spec.Groups), nil +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 6793a2a2..7969c2dd 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -19,13 +19,16 @@ limitations under the License. package handler import ( + "context" "fmt" "net/http" + "github.com/unikorn-cloud/core/pkg/authorization/roles" "github.com/unikorn-cloud/core/pkg/authorization/userinfo" "github.com/unikorn-cloud/core/pkg/server/errors" "github.com/unikorn-cloud/identity/pkg/authorization" "github.com/unikorn-cloud/identity/pkg/generated" + "github.com/unikorn-cloud/identity/pkg/handler/groups" "github.com/unikorn-cloud/identity/pkg/handler/oauth2providers" "github.com/unikorn-cloud/identity/pkg/handler/organizations" "github.com/unikorn-cloud/identity/pkg/util" @@ -47,6 +50,19 @@ type Handler struct { options *Options } +func checkRBAC(ctx context.Context, organization, scope string, permission roles.Permission) error { + authorizer, err := userinfo.NewAuthorizer(ctx, organization) + if err != nil { + return errors.HTTPForbidden("operation is not allowed by rbac").WithError(err) + } + + if err := authorizer.Allow(scope, permission); err != nil { + return errors.HTTPForbidden("operation is not allowed by rbac").WithError(err) + } + + return nil +} + func New(client client.Client, namespace string, authenticator *authorization.Authenticator, options *Options) (*Handler, error) { h := &Handler{ client: client, @@ -183,6 +199,19 @@ func (h *Handler) PutApiV1OrganizationsOrganization(w http.ResponseWriter, r *ht } func (h *Handler) GetApiV1OrganizationsOrganizationGroups(w http.ResponseWriter, r *http.Request, organization generated.OrganizationParameter) { + if err := checkRBAC(r.Context(), organization, "groups", roles.Read); err != nil { + errors.HandleError(w, r, err) + return + } + + result, err := groups.New(h.client, h.namespace).List(r.Context(), organization) + if err != nil { + errors.HandleError(w, r, err) + return + } + + h.setUncacheable(w) + util.WriteJSONResponse(w, r, http.StatusOK, result) } func (h *Handler) PostApiV1OrganizationsOrganizationGroups(w http.ResponseWriter, r *http.Request, organization generated.OrganizationParameter) {