From da2f081a3e620ede69b4b4c20113a529da3eb018 Mon Sep 17 00:00:00 2001 From: bjwswang Date: Tue, 7 Feb 2023 19:14:31 +0800 Subject: [PATCH] feat: new/add webhooks to channel\organization\federation;fix dir permission error on fabric sdk --- PROJECT | 4 + api/v1beta1/channel_webhook.go | 196 ++++++++++++++++++ api/v1beta1/federation_webhook.go | 33 ++- api/v1beta1/network_webhook.go | 3 +- api/v1beta1/organization_webhook.go | 20 +- api/v1beta1/webhook.go | 4 + api/v1beta1/webhook_suite_test.go | 3 + .../samples/ibp.com_v1beta1_channel_join.yaml | 2 +- config/webhook/manifests.yaml | 43 +++- pkg/connector/profile.go | 18 ++ 10 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 api/v1beta1/channel_webhook.go diff --git a/PROJECT b/PROJECT index 30212f27..a3b3ee44 100644 --- a/PROJECT +++ b/PROJECT @@ -103,4 +103,8 @@ resources: kind: Channel path: github.com/IBM-Blockchain/fabric-operator/api/v1beta1 version: v1beta1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1beta1/channel_webhook.go b/api/v1beta1/channel_webhook.go new file mode 100644 index 00000000..bcb9c050 --- /dev/null +++ b/api/v1beta1/channel_webhook.go @@ -0,0 +1,196 @@ +/* + * Copyright contributors to the Hyperledger Fabric Operator project + * + * SPDX-License-Identifier: Apache-2.0 + * + * 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 v1beta1 + +import ( + "context" + + "github.com/pkg/errors" + authenticationv1 "k8s.io/api/authentication/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var ( + errNoNetwork = errors.New("cant find network") + errMemberNotInNetwork = errors.New("some channel member not in network") + errNoPermOperatePeer = errors.New("no permission to operate peer") + errUpdateChannelNetwork = errors.New("cant update channel's network") + errUpdateChannelMember = errors.New("cant update channel's members directly(must use proposal-vote)") + errChannelHasPeers = errors.New("channel still have peers joined") +) + +// log is for logging in this package. +var channellog = logf.Log.WithName("channel-resource") + +//+kubebuilder:webhook:path=/mutate-ibp-com-v1beta1-channel,mutating=true,failurePolicy=fail,sideEffects=None,groups=ibp.com,resources=channels,verbs=create;update,versions=v1beta1,name=channel.mutate.webhook,admissionReviewVersions=v1 + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *Channel) Default(ctx context.Context, client client.Client, user authenticationv1.UserInfo) { + channellog.Info("default", "name", r.Name, "user", user.String()) +} + +//+kubebuilder:webhook:path=/validate-ibp-com-v1beta1-channel,mutating=false,failurePolicy=fail,sideEffects=None,groups=ibp.com,resources=channels,verbs=create;update;delete,versions=v1beta1,name=channel.validate.webhook,admissionReviewVersions=v1 + +var _ validator = &Channel{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Channel) ValidateCreate(ctx context.Context, c client.Client, user authenticationv1.UserInfo) error { + channellog.Info("validate create", "name", r.Name, "user", user.String()) + + err := validateMemberInNetwork(ctx, c, r.Spec.Network, r.Spec.Members) + if err != nil { + return err + } + + // managedOrgs which this user can manage + managedOrgs, err := filterManagedOrgs(ctx, c, user, r.Spec.Members) + if err != nil { + return err + } + // initialized peers should under user's management + err = validatePeersOwnership(ctx, c, managedOrgs, r.Spec.Peers) + if err != nil { + return err + } + + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Channel) ValidateUpdate(ctx context.Context, c client.Client, old runtime.Object, user authenticationv1.UserInfo) error { + channellog.Info("validate update", "name", r.Name, "user", user.String()) + + oldChannel := old.(*Channel) + + // forbid to udpate channel network + if oldChannel.Spec.Network != r.Spec.Network { + return errUpdateChannelNetwork + } + + // forbid to update channel members directly + added, removed := DifferMembers(oldChannel.Spec.Members, r.Spec.Members) + if len(added) != 0 || len(removed) != 0 { + return errUpdateChannelMember + } + + // forbid to update peers which not belongs to user's organizations + addedPeers, removedPeers := DifferChannelPeers(oldChannel.Spec.Peers, r.Spec.Peers) + if len(addedPeers) != 0 || len(removedPeers) != 0 { + managedOrgs, err := filterManagedOrgs(ctx, c, user, r.Spec.Members) + if err != nil { + return err + } + // updated peers should under user's management + err = validatePeersOwnership(ctx, c, managedOrgs, append(addedPeers, removedPeers...)) + if err != nil { + return err + } + } + + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Channel) ValidateDelete(ctx context.Context, client client.Client, user authenticationv1.UserInfo) error { + channellog.Info("validate delete", "name", r.Name, "user", user.String()) + + // forbid to delete channel if still have peers joined + if len(r.Spec.Peers) != 0 { + return errors.Wrapf(errChannelHasPeers, "count %d", len(r.Spec.Peers)) + } + + return nil +} + +func validateMemberInNetwork(ctx context.Context, c client.Client, netName string, members []Member) error { + net := &Network{} + net.Name = netName + if err := c.Get(ctx, client.ObjectKeyFromObject(net), net); err != nil { + if apierrors.IsNotFound(err) { + return errNoNetwork + } + return errors.Wrap(err, "failed to get network") + } + if net.Status.Type == Error { + return errors.Errorf("network %s has error %s:%s", netName, net.Status.Reason, net.Status.Message) + } + + allMembers := make(map[string]bool, len(net.Spec.Members)) + for _, m := range net.Spec.Members { + allMembers[m.Name] = true + } + for _, m := range members { + if ok := allMembers[m.Name]; !ok { + return errors.Wrapf(errMemberNotInNetwork, "allMembers:%#v, members:%#v", allMembers, members) + } + } + return nil +} + +// filterManagedOrgs will get the organizations which under user's management +func filterManagedOrgs(ctx context.Context, c client.Client, user authenticationv1.UserInfo, members []Member) ([]string, error) { + var err error + + // managedOrgs which this user can manage + managedOrgs := make([]string, 0) + // validate ownership + for _, member := range members { + org := &Organization{} + org.Name = member.Name + err = c.Get(ctx, client.ObjectKeyFromObject(org), org) + if err != nil { + return nil, errors.Wrap(err, "failed to get organization") + } + if isSuperUser(ctx, user) || org.Spec.Admin == user.Username { + managedOrgs = append(managedOrgs, member.Name) + } + } + + return managedOrgs, nil +} + +// validatePeersOwnership validate whether peers belongs to ownerOrgs +func validatePeersOwnership(ctx context.Context, c client.Client, ownerOrgs []string, peers []NamespacedName) error { + // cache owners + owners := make(map[string]bool, len(ownerOrgs)) + for _, ownerOrg := range ownerOrgs { + owners[ownerOrg] = true + } + // make sure peers run in ownerOrgs + for _, peer := range peers { + // peer must belongs to owners + if !owners[peer.Namespace] { + return errors.Wrapf(errNoPermOperatePeer, "peer belongs to %s not in %v", peer.Namespace, ownerOrgs) + } + p := &IBPPeer{} + err := c.Get(ctx, types.NamespacedName{Namespace: peer.Namespace, Name: peer.Name}, p) + if err != nil { + return errors.Wrapf(err, "failed to get peer %s", peer.String()) + } + if p.Status.Type == Error { + return errors.Errorf("peer %s has error %s:%s", peer.String(), p.Status.Reason, p.Status.Message) + } + } + + return nil +} diff --git a/api/v1beta1/federation_webhook.go b/api/v1beta1/federation_webhook.go index 51afbf35..a67ed971 100644 --- a/api/v1beta1/federation_webhook.go +++ b/api/v1beta1/federation_webhook.go @@ -49,7 +49,7 @@ func (r *Federation) Default(ctx context.Context, client client.Client, user aut federationlog.Info("default", "name", r.Name, "user", user.String()) } -//+kubebuilder:webhook:path=/validate-ibp-com-v1beta1-federation,mutating=false,failurePolicy=fail,sideEffects=None,groups=ibp.ibp.com,resources=federations,verbs=create;update;delete,versions=v1beta1,name=federation.validate.webhook,admissionReviewVersions=v1 +//+kubebuilder:webhook:path=/validate-ibp-com-v1beta1-federation,mutating=false,failurePolicy=fail,sideEffects=None,groups=ibp.com,resources=federations,verbs=create;update;delete,versions=v1beta1,name=federation.validate.webhook,admissionReviewVersions=v1 var _ validator = &Federation{} @@ -64,6 +64,13 @@ func (r *Federation) ValidateCreate(ctx context.Context, client client.Client, u return err } + for _, member := range r.Spec.Members { + err := validateOrganization(ctx, client, member.Name) + if err != nil { + return err + } + } + return nil } @@ -90,6 +97,14 @@ func (r *Federation) ValidateDelete(ctx context.Context, client client.Client, u return err } + if r.Status.Type != FederationFailed && r.Status.Type != FederationDissolved { + return errors.Errorf("forbid to delete federation when it is at status %s", r.Status.Type) + } + + if len(r.Status.Networks) != 0 { + return errors.Errorf("forbid to delete federation when it still has %d networks", len(r.Status.Networks)) + } + return nil } @@ -118,3 +133,19 @@ func validateInitiator(ctx context.Context, c client.Client, user authentication } return nil } + +func validateOrganization(ctx context.Context, c client.Client, organization string) error { + federationlog.Info("validate organization: %s", organization) + org := &Organization{} + org.Name = organization + err := c.Get(ctx, client.ObjectKeyFromObject(org), org) + if err != nil { + return errors.Wrapf(err, "failed to get organization %s", organization) + } + + if org.Status.Type == Error { + return errors.Errorf("organization %s has error %s:%s", org.Name, org.Status.Reason, org.Status.Message) + } + + return nil +} diff --git a/api/v1beta1/network_webhook.go b/api/v1beta1/network_webhook.go index 448a975c..acf62981 100644 --- a/api/v1beta1/network_webhook.go +++ b/api/v1beta1/network_webhook.go @@ -20,7 +20,6 @@ package v1beta1 import ( "context" - "fmt" "github.com/pkg/errors" @@ -115,7 +114,7 @@ func validateMemberInFederation(ctx context.Context, c client.Client, fedName st } for _, m := range members { if ok := allMembers[m.Name]; !ok { - return fmt.Errorf("allMembers:%#v, members:%#v", allMembers, members) + return errors.Wrapf(errMemberNotInFederation, "allMembers:%#v, members:%#v", allMembers, members) } } return nil diff --git a/api/v1beta1/organization_webhook.go b/api/v1beta1/organization_webhook.go index 74e628d6..b5f53bda 100644 --- a/api/v1beta1/organization_webhook.go +++ b/api/v1beta1/organization_webhook.go @@ -29,7 +29,9 @@ import ( ) var ( - errHasNetwork = errors.New("the organization is initiator of one network") + errAdminIsEmpty = errors.New("the organization's admin is empty") + errHasNetwork = errors.New("the organization is initiator of one network") + errHasFederation = errors.New("the organization is initiator of one federation") ) // log is for logging in this package. @@ -67,6 +69,9 @@ func (r *Organization) ValidateUpdate(ctx context.Context, client client.Client, if !isSuperUser(ctx, user) && oldOrg.Spec.Admin != user.Username { return errNoPermission } + if r.Spec.Admin == "" { + return errAdminIsEmpty + } return nil } @@ -76,6 +81,19 @@ func (r *Organization) ValidateDelete(ctx context.Context, c client.Client, user if !isSuperUser(ctx, user) && r.Spec.Admin != user.Username { return errNoPermission } + // validate whether the organization is the initiator of a federation(initiator responsibility) + federationList := &FederationList{} + if err := c.List(ctx, federationList); err != nil { + return errors.Wrap(err, "cant get federation list") + } + for _, fed := range federationList.Items { + for _, m := range fed.Spec.Members { + if m.Initiator && m.Name == r.Name { + return errHasFederation + } + } + } + // validate whether the organization is the initiator of a network(initiator responsibility) networkList := &NetworkList{} if err := c.List(ctx, networkList); err != nil { return errors.Wrap(err, "cant get network list") diff --git a/api/v1beta1/webhook.go b/api/v1beta1/webhook.go index 62656d80..0933d7ef 100644 --- a/api/v1beta1/webhook.go +++ b/api/v1beta1/webhook.go @@ -69,6 +69,10 @@ func AddWebhooks(mgr ctrl.Manager, setupLog logr.Logger) (err error) { setupLog.Error(err, "unable to create webhook", "webhook", "Organization") return err } + if err = registerCustomWebhook(mgr, &Channel{}, operatorUser); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Channel") + return err + } return nil } diff --git a/api/v1beta1/webhook_suite_test.go b/api/v1beta1/webhook_suite_test.go index 7021f8d7..d1978c0e 100644 --- a/api/v1beta1/webhook_suite_test.go +++ b/api/v1beta1/webhook_suite_test.go @@ -116,6 +116,9 @@ var _ = BeforeSuite(func() { err = registerCustomWebhook(mgr, &Federation{}, fakeOperatorUser) Expect(err).NotTo(HaveOccurred()) + err = registerCustomWebhook(mgr, &Channel{}, fakeOperatorUser) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:webhook go func() { diff --git a/config/samples/ibp.com_v1beta1_channel_join.yaml b/config/samples/ibp.com_v1beta1_channel_join.yaml index 76bb20c2..be2e18bc 100644 --- a/config/samples/ibp.com_v1beta1_channel_join.yaml +++ b/config/samples/ibp.com_v1beta1_channel_join.yaml @@ -13,5 +13,5 @@ spec: - name: org1peer1 namespace: org1 - name: org2peer1 - namespace: org1 + namespace: org2 description: "channel with org1 & org2" diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 5a8d9344..73a334c5 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -5,6 +5,26 @@ metadata: creationTimestamp: null name: mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-ibp-com-v1beta1-channel + failurePolicy: Fail + name: channel.mutate.webhook + rules: + - apiGroups: + - ibp.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - channels + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -112,6 +132,27 @@ metadata: creationTimestamp: null name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-ibp-com-v1beta1-channel + failurePolicy: Fail + name: channel.validate.webhook + rules: + - apiGroups: + - ibp.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - channels + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -123,7 +164,7 @@ webhooks: name: federation.validate.webhook rules: - apiGroups: - - ibp.ibp.com + - ibp.com apiVersions: - v1beta1 operations: diff --git a/pkg/connector/profile.go b/pkg/connector/profile.go index c67e53a5..d85fd511 100644 --- a/pkg/connector/profile.go +++ b/pkg/connector/profile.go @@ -23,6 +23,7 @@ import ( "encoding/base64" "encoding/json" "net/url" + "path/filepath" current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1" "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient" @@ -50,6 +51,7 @@ type Client struct { Organization string `yaml:"organization,omitempty"` Logging `yaml:"logging,omitempty"` // CryptoConfig `yaml:"cryptoconfig,omitempty"` + CredentialStore `yaml:"credentialStore,omitempty"` } type Logging struct { @@ -60,6 +62,15 @@ type CryptoConfig struct { Path string `yaml:"path,omitempty"` } +type CredentialStore struct { + Path string `yaml:"path,omitempty"` + CryptoStore `yaml:"cryptoStore,omitempty"` +} + +type CryptoStore struct { + Path string `yaml:"path,omitempty"` +} + // ChannelInfo defines configurations when connect to this channel type ChannelInfo struct { // Peers which can be used to connect to this channel @@ -121,6 +132,13 @@ func DefaultClient(baseDir string, org string) *Client { Logging: Logging{ Level: "info", }, + // Must specify CredentialStore to avoid `mkdir keystore permission error` + CredentialStore: CredentialStore{ + Path: filepath.Join(baseDir, org, "hfc-kvs"), + CryptoStore: CryptoStore{ + Path: filepath.Join(baseDir, org, "hfc-cvs"), + }, + }, // CryptoConfig: CryptoConfig{ // Path: baseDir, // },