Skip to content

Commit

Permalink
Merge pull request #3486 from barney-s/cidentmgr-mock-a
Browse files Browse the repository at this point in the history
Adding mocks for cloudidentity membership API
  • Loading branch information
google-oss-prow[bot] authored Jan 23, 2025
2 parents a361cd2 + 538f073 commit d3cf43e
Show file tree
Hide file tree
Showing 23 changed files with 1,784 additions and 154 deletions.
1 change: 1 addition & 0 deletions config/tests/samples/create/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured
case schema.GroupKind{Group: "cloudids.cnrm.cloud.google.com", Kind: "CloudIDSEndpoint"}:

case schema.GroupKind{Group: "cloudidentity.cnrm.cloud.google.com", Kind: "CloudIdentityGroup"}:
case schema.GroupKind{Group: "cloudidentity.cnrm.cloud.google.com", Kind: "CloudIdentityMembership"}:

case schema.GroupKind{Group: "containerattached.cnrm.cloud.google.com", Kind: "ContainerAttachedCluster"}:

Expand Down
14 changes: 0 additions & 14 deletions mockgcp/mockcloudidentity/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/klog/v2"

Expand Down Expand Up @@ -133,19 +132,6 @@ func addAdditionalGroup(obj *pb.Group, entityKey *pb.EntityKey) {
obj.AdditionalGroupKeys = append(obj.AdditionalGroupKeys, entityKey)
}

func buildLRO(obj proto.Message) (*longrunning.Operation, error) {
responseAny, err := anypb.New(obj)
if err != nil {
return nil, fmt.Errorf("error building anypb for response: %w", err)
}
lro := &longrunning.Operation{}
lro.Done = true
lro.Result = &longrunning.Operation_Response{
Response: responseAny,
}
return lro, nil
}

func (s *groupsServer) PatchGroup(ctx context.Context, req *pb.PatchGroupRequest) (*longrunning.Operation, error) {
reqName := req.GetName()

Expand Down
222 changes: 222 additions & 0 deletions mockgcp/mockcloudidentity/groupsmembership.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright 2024 Google LLC
//
// 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 mockcloudidentity

import (
"context"
"fmt"
"strings"
"time"

"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"

pb "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/generated/google/apps/cloudidentity/groups/v1beta1"
)

type groupsMembershipsServer struct {
*MockService
pb.UnimplementedGroupsMembershipsServerServer
}

func (s *groupsMembershipsServer) GetGroupsMembership(ctx context.Context, req *pb.GetGroupsMembershipRequest) (*pb.Membership, error) {
//GET https://cloudidentity.googleapis.com/v1/{name=groups/*/memberships/*}
name, err := s.parseMembershipName(req.GetName())
if err != nil {
return nil, err
}

fqn := name.String()

obj := &pb.Membership{}
if err := s.storage.Get(ctx, fqn, obj); err != nil {
if status.Code(err) == codes.NotFound {
return nil, status.Errorf(codes.PermissionDenied, "Error(2017): Permission denied for group resource '%s' (or it may not exist).", fqn)
}
return nil, err
}

return obj, nil
}

func (s *groupsMembershipsServer) CreateGroupsMembership(ctx context.Context, req *pb.CreateGroupsMembershipRequest) (*longrunning.Operation, error) {
// POST https://cloudidentity.googleapis.com/v1/{parent=groups/*}/memberships
groupName, err := s.parseGroupName(*req.Parent)
if err != nil {
return nil, err
}
reqName := fmt.Sprintf("%s/memberships/%x", groupName.String(), time.Now().UnixMilli())
name, err := s.parseMembershipName(reqName)
if err != nil {
return nil, err
}

fqn := name.String()

now := timestamppb.Now()

obj := proto.Clone(req.GroupsMembership).(*pb.Membership)
obj.Name = PtrTo(fmt.Sprintf("%s", name.String()))
obj.CreateTime = now
obj.UpdateTime = now

obj.Type = PtrTo("USER") // TODO: logic for this value ?
obj.DeliverySetting = PtrTo("DIGEST") // TODO: logic for this value ?

if err := s.storage.Create(ctx, fqn, obj); err != nil {
return nil, err
}

// TODO: is this all for LRO ?
retObj := proto.Clone(obj).(*pb.Membership)
return buildLRO(retObj)
}

// No patch in mockgcp/generated/google/apps/cloudidentity/groups/v1beta1/service_grpc.pb.go
// func (s *groupsMembershipsServer) PatchGroupsMembership(ctx context.Context, req *pb.PatchGroupsMembershipRequest) (*longrunning.Operation, error) {}

// TODO: implement these from mockgcp/generated/google/apps/cloudidentity/groups/v1beta1/service_grpc.pb.go ?
// Modifies the `MembershipRole`s of a `Membership`.
// ModifyMembershipRolesGroupsMembership(ctx context.Context, in *ModifyMembershipRolesGroupsMembershipRequest, opts ...grpc.CallOption) (*ModifyMembershipRolesResponse, error)
// Searches direct groups of a member.
// SearchDirectGroupsGroupsMembership(ctx context.Context, in *SearchDirectGroupsGroupsMembershipRequest, opts ...grpc.CallOption) (*SearchDirectGroupsResponse, error)
// Search transitive groups of a member. **Note:** This feature is only available to Google Workspace Enterprise Standard, Enterprise Plus, and Enterprise for Education; and Cloud Identity Premium accounts. A transitive group is any group that has a direct or indirect membership to the member. Actor must have view permissions all transitive groups.
// SearchTransitiveGroupsGroupsMembership(ctx context.Context, in *SearchTransitiveGroupsGroupsMembershipRequest, opts ...grpc.CallOption) (*SearchTransitiveGroupsResponse, error)
// Search transitive memberships of a group. **Note:** This feature is only available to Google Workspace Enterprise Standard, Enterprise Plus, and Enterprise for Education; and Cloud Identity Premium accounts. A transitive membership is any direct or indirect membership of a group. Actor must have view permissions to all transitive memberships.
// SearchTransitiveMembershipsGroupsMembership(ctx context.Context, in *SearchTransitiveMembershipsGroupsMembershipRequest, opts ...grpc.CallOption) (*SearchTransitiveMembershipsResponse, error)

func (s *groupsMembershipsServer) ModifyMembershipRolesGroupsMembership(ctx context.Context, req *pb.ModifyMembershipRolesGroupsMembershipRequest) (*pb.ModifyMembershipRolesResponse, error) {
name, err := s.parseMembershipName(req.GetName())
if err != nil {
return nil, err
}

fqn := name.String()

obj := &pb.Membership{}
if err := s.storage.Get(ctx, fqn, obj); err != nil {
if status.Code(err) == codes.NotFound {
return nil, status.Errorf(codes.PermissionDenied, "Error(2017): Permission denied for group resource '%s' (or it may not exist).", fqn)
}
return nil, err
}

retObj := proto.Clone(obj).(*pb.Membership)
response := &pb.ModifyMembershipRolesResponse{
Membership: retObj,
}

gm := req.GetGroupsMembership()
if gm == nil {
return response, nil
}

for i := range gm.RemoveRoles {
for j := range retObj.Roles {
if gm.RemoveRoles[i] == *retObj.Roles[j].Name {
retObj.Roles = append(retObj.Roles[:j], retObj.Roles[j+1:]...)
break
}
}
}

for _, role := range gm.AddRoles {
retObj.Roles = append(retObj.Roles, role)
}

// From proto defn:
// The fully-qualified names of fields to update. May only contain the field `expiry_detail.expire_time`.
// FieldMask *string `protobuf:"bytes,1,opt,name=field_mask,json=fieldMask" json:"field_mask,omitempty"`
// The `MembershipRole`s to be updated. Only `MEMBER` `MembershipRoles` can currently be updated. May only contain a `MembershipRole` with `name` `MEMBER`.
// MembershipRole *MembershipRole `protobuf:"bytes,2,opt,name=membership_role,json=membershipRole" json:"membership_role,omitempty"`
for i := range gm.UpdateRolesParams {
umr := gm.UpdateRolesParams[i].GetMembershipRole()
if umr == nil {
continue
}
if *umr.Name != "MEMBER" {
continue
}
if umr.ExpiryDetail == nil {
continue
}
if umr.ExpiryDetail.ExpireTime == nil {
continue
}
if *gm.UpdateRolesParams[i].FieldMask != "expiry_detail.expire_time" {
continue
}

for j := range retObj.Roles {
if *umr.Name == *retObj.Roles[j].Name {
retObj.Roles[j].ExpiryDetail = proto.Clone(umr.ExpiryDetail).(*pb.ExpiryDetail)
break
}
}
}

if err := s.storage.Update(ctx, fqn, retObj); err != nil {
return nil, err
}
return response, nil
}

func (s *groupsMembershipsServer) DeleteGroupsMembership(ctx context.Context, req *pb.DeleteGroupsMembershipRequest) (*longrunning.Operation, error) {
name, err := s.parseMembershipName(req.GetName())
if err != nil {
return nil, err
}

fqn := name.String()

deleted := &pb.Membership{}
if err := s.storage.Delete(ctx, fqn, deleted); err != nil {
return nil, err
}

// Returns a non-standard LRO
lro := &longrunning.Operation{}
lro.Done = true
return lro, nil
}

type membershipName struct {
Group string
Membership string
}

func (n *membershipName) String() string {
return fmt.Sprintf("groups/%s/memberships/%s", n.Group, n.Membership)
}

func (s *MockService) parseMembershipName(name string) (*membershipName, error) {
//From GET https://cloudidentity.googleapis.com/v1/{name=groups/*/memberships/*}
// name: groups/{group}/memberships/{membership}
tokens := strings.Split(name, "/")

if len(tokens) == 4 && tokens[0] == "groups" && tokens[2] == "memberships" {
name := &membershipName{
Group: tokens[1],
Membership: tokens[3],
}

return name, nil
} else {
return nil, status.Errorf(codes.InvalidArgument, "name %q is not valid", name)
}
}
19 changes: 18 additions & 1 deletion mockgcp/mockcloudidentity/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package mockcloudidentity
import (
"context"
"net/http"
"strings"

"google.golang.org/grpc"

Expand Down Expand Up @@ -52,16 +53,32 @@ func (s *MockService) ExpectedHosts() []string {

func (s *MockService) Register(grpcServer *grpc.Server) {
pb.RegisterGroupsServerServer(grpcServer, &groupsServer{MockService: s})
pb.RegisterGroupsMembershipsServerServer(grpcServer, &groupsMembershipsServer{MockService: s})
}

func (s *MockService) NewHTTPMux(ctx context.Context, conn *grpc.ClientConn) (http.Handler, error) {
mux, err := httpmux.NewServeMux(ctx, conn, httpmux.Options{},
pb.RegisterGroupsServerHandler,
pb.RegisterGroupsMembershipsServerHandler,
s.operations.RegisterOperationsPath("/v1beta1/operations/{name}"),
)
if err != nil {
return nil, err
}

return mux, nil
// DCL sends a trailing slash, but that is not technically correct
// and it trips up grpc-gateway (https://github.com/grpc-ecosystem/grpc-gateway/issues/472)
// e.g. POST https://cloudidentity.googleapis.com/v1beta1/groups/1946c1f7c26/memberships/?alt=json
removeTrailingSlash := func(w http.ResponseWriter, r *http.Request) {
u := r.URL
if strings.HasSuffix(u.Path, "/memberships/") {
u2 := *u
u2.Path = strings.TrimSuffix(u2.Path, "/")
r = httpmux.RewriteRequest(r, &u2)
}

mux.ServeHTTP(w, r)
}

return http.HandlerFunc(removeTrailingSlash), nil
}
21 changes: 21 additions & 0 deletions mockgcp/mockcloudidentity/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

package mockcloudidentity

import (
"fmt"

"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)

func PtrTo[T any](t T) *T {
return &t
}
Expand All @@ -25,3 +33,16 @@ func ValueOf[T any](p *T) T {
}
return v
}

func buildLRO(obj proto.Message) (*longrunning.Operation, error) {
responseAny, err := anypb.New(obj)
if err != nil {
return nil, fmt.Errorf("error building anypb for response: %w", err)
}
lro := &longrunning.Operation{}
lro.Done = true
lro.Result = &longrunning.Operation_Response{
Response: responseAny,
}
return lro, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
apiVersion: cloudidentity.cnrm.cloud.google.com/v1beta1
kind: CloudIdentityMembership
metadata:
annotations:
cnrm.cloud.google.com/management-conflict-prevention-policy: none
cnrm.cloud.google.com/state-into-spec: absent
finalizers:
- cnrm.cloud.google.com/finalizer
- cnrm.cloud.google.com/deletion-defender
generation: 3
labels:
cnrm-test: "true"
name: cloudidentitymembership-${uniqueId}
namespace: ${uniqueId}
spec:
groupRef:
name: cloudidentitygroup-${uniqueId}
preferredMemberKey:
id: test2@${ISOLATED_TEST_ORG_NAME}
resourceID: ${membershipID}
roles:
- expiryDetail:
expireTime: "2222-10-02T15:01:23Z"
name: MEMBER
status:
conditions:
- lastTransitionTime: "1970-01-01T00:00:00Z"
message: The resource is up to date
reason: UpToDate
status: "True"
type: Ready
createTime: "1970-01-01T00:00:00Z"
deliverySetting: DIGEST
observedGeneration: 3
type: USER
updateTime: "1970-01-01T00:00:00Z"
Loading

0 comments on commit d3cf43e

Please sign in to comment.