diff --git a/pkg/ctl/namespace/errors_ns.go b/pkg/ctl/namespace/errors_ns.go index 3ec66f561..df191f85a 100644 --- a/pkg/ctl/namespace/errors_ns.go +++ b/pkg/ctl/namespace/errors_ns.go @@ -24,6 +24,11 @@ var ArgError = pulsar.Output{ Out: "[✖] only one argument is allowed to be used as a name", } +var AuthNotEnable = pulsar.Output{ + Desc: "the authorization is not enabled", + Out: "[✖] code: 501 reason: Authorization is not enabled", +} + var NsNotExistError = pulsar.Output{ Desc: "the specified namespace name does not exist", Out: "[✖] code: 404 reason: Namespace does not exist", diff --git a/pkg/ctl/namespace/grant_permission.go b/pkg/ctl/namespace/grant_permission.go new file mode 100644 index 000000000..27dc2648e --- /dev/null +++ b/pkg/ctl/namespace/grant_permission.go @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/streamnative/pulsarctl/pkg/cmdutils" + "github.com/streamnative/pulsarctl/pkg/pulsar" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func GrantPermissionsCmd(vc *cmdutils.VerbCmd) { + var desc pulsar.LongDescription + desc.CommandUsedFor = "This command is used for granting permissions to a client role to access a namespace." + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []pulsar.Example + grant := pulsar.Example{ + Desc: "Grant permission (action) to the client role (role-name) to access the namespace (namespace-name)", + Command: "pulsarctl namespaces grant-permission --role (role-name) --actions (action) (namespace-name)", + } + + grantActions := pulsar.Example{ + Desc: "Grant permissions (actions) to the client role (role-name) to access the namespace (namespace-name)", + Command: "pulsarctl namespaces grant-permission --role (role-name) --actions (action-1) --actions (action-2) " + + "(namespace-name)", + } + examples = append(examples, grant, grantActions) + desc.CommandExamples = examples + + var out []pulsar.Output + successOut := pulsar.Output{ + Desc: "normal output", + Out: "Grant permissions (actions) to the client role (role-name) to access the namespace (namespace-name)" + + " successfully", + } + out = append(out, successOut, ArgError, AuthNotEnable) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "grant-permission", + "Grant permissions to a client role to access a namespace", + desc.ToString(), + desc.ExampleToString()) + + var role string + var actions []string + + vc.SetRunFuncWithNameArg(func() error { + return doGrantPermissions(vc, role, actions) + }) + + vc.FlagSetGroup.InFlagSet("Grant Permissions", func(set *pflag.FlagSet) { + set.StringVar(&role, "role", "", + "Client role to which grant permissions") + set.StringSliceVar(&actions, "actions", []string{}, + "Actions to be granted (produce,consume,functions)") + cobra.MarkFlagRequired(set, "role") + cobra.MarkFlagRequired(set, "actions") + }) +} + +func doGrantPermissions(vc *cmdutils.VerbCmd, role string, actions []string) error { + // for testing + if vc.NameError != nil { + return vc.NameError + } + + ns, err := pulsar.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + a, err := parseActions(actions) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().GrantNamespacePermission(*ns, role, a) + if err == nil { + vc.Command.Printf("Grant permissions %+v to the client role %s to access the"+ + " namespace %s successfully\n", a, role, ns.String()) + } + + return err +} + +func parseActions(actions []string) ([]pulsar.AuthAction, error) { + r := make([]pulsar.AuthAction, 0) + for _, v := range actions { + a, err := pulsar.ParseAuthAction(v) + if err != nil { + return nil, err + } + r = append(r, a) + } + return r, nil +} diff --git a/pkg/ctl/namespace/grant_permission_test.go b/pkg/ctl/namespace/grant_permission_test.go new file mode 100644 index 000000000..5a07b8e21 --- /dev/null +++ b/pkg/ctl/namespace/grant_permission_test.go @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGrantPermissionsCmd(t *testing.T) { + ns := "public/test-grant-permissions-ns" + + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"grant-permission", "--role", "test-permissions", + "--actions", "produce", "--actions", "consume", "--actions", "functions", ns} + _, execErr, _, _ = TestNamespaceCommands(GrantPermissionsCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "code: 501 reason: Authorization is not enabled", execErr.Error()) +} + +func TestGrantPermissionsArgsError(t *testing.T) { + ns := "public/grant-permissions-tests" + + args := []string{"grant-permission", ns} + _, _, _, err := TestNamespaceCommands(GrantPermissionsCmd, args) + assert.NotNil(t, err) + assert.Equal(t, "required flag(s) \"actions\", \"role\" not set", err.Error()) + + args = []string{"grant-permission", "--role", "test-role", "--actions", "consume"} + _, _, nameErr, _ := TestNamespaceCommands(GrantPermissionsCmd, args) + assert.NotNil(t, nameErr) + assert.Equal(t, "only one argument is allowed to be used as a name", nameErr.Error()) + + args = []string{"grant-permission", "--role", "test-role", "--actions", "fail", ns} + _, execErr, _, _ := TestNamespaceCommands(GrantPermissionsCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "The auth action only can be specified as 'produce', "+ + "'consume', or 'functions'. Invalid auth action 'fail'", execErr.Error()) +} diff --git a/pkg/ctl/namespace/grant_subscription_permission.go b/pkg/ctl/namespace/grant_subscription_permission.go new file mode 100644 index 000000000..faa8ef56d --- /dev/null +++ b/pkg/ctl/namespace/grant_subscription_permission.go @@ -0,0 +1,102 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/streamnative/pulsarctl/pkg/cmdutils" + "github.com/streamnative/pulsarctl/pkg/pulsar" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func GrantSubPermissionsCmd(vc *cmdutils.VerbCmd) { + var desc pulsar.LongDescription + desc.CommandUsedFor = "This command is used for granting client roles to access a subscription of a namespace." + desc.CommandPermission = "This command requires super-user permissions." + + var examples []pulsar.Example + grant := pulsar.Example{ + Desc: "Grant the client roles (roles-name) to access the subscription (subscription-name) of the " + + "namespace (namespace-name)", + Command: "pulsarctl namespaces grant-subscription-permission --role (role1-name) --role (role2-name) " + + "(namespace-name) (subscription-name)", + } + examples = append(examples, grant) + desc.CommandExamples = examples + + var out []pulsar.Output + successOut := pulsar.Output{ + Desc: "normal output", + Out: "Grant the client role to access the subscription of the " + + "namespace successfully", + } + + argsError := pulsar.Output{ + Desc: "the namespace name is not specified or the subscription name is not specified", + Out: "[✖] need to specified namespace name and subscription name", + } + out = append(out, successOut, argsError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "grant-subscription-permission", + "Grant a client role to access a subscription of a namespace", + desc.ToString(), + desc.ExampleToString()) + + var role []string + + vc.SetRunFuncWithMultiNameArgs(func() error { + return doGrantSubscriptionPermissions(vc, role) + }, func(args []string) error { + if len(args) != 2 { + return errors.New("need to specified namespace name and subscription name") + } + return nil + }) + + vc.FlagSetGroup.InFlagSet("Grant Subscription Permissions", func(set *pflag.FlagSet) { + set.StringSliceVar(&role, "role", nil, + "Client role to which grant permissions") + cobra.MarkFlagRequired(set, "role") + }) +} + +func doGrantSubscriptionPermissions(vc *cmdutils.VerbCmd, role []string) error { + // for testing + if vc.NameError != nil { + return vc.NameError + } + + ns, err := pulsar.GetNamespaceName(vc.NameArgs[0]) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().GrantSubPermission(*ns, vc.NameArgs[1], role) + if err == nil { + vc.Command.Printf("Grant the client role %+v to access the subscription %s of "+ + "the namespace %s successfully\n", role, vc.NameArgs[1], ns.String()) + } + + return err +} diff --git a/pkg/ctl/namespace/grant_subscription_permission_test.go b/pkg/ctl/namespace/grant_subscription_permission_test.go new file mode 100644 index 000000000..146cc8655 --- /dev/null +++ b/pkg/ctl/namespace/grant_subscription_permission_test.go @@ -0,0 +1,56 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGrantSubPermissionsCmd(t *testing.T) { + ns := "public/test-grant-sub-permissions-ns" + + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"grant-subscription-permission", "--role", "test-permissions", ns, "test-grant-sub"} + _, execErr, _, _ = TestNamespaceCommands(GrantSubPermissionsCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "code: 501 reason: Authorization is not enabled", execErr.Error()) +} + +func TestGrantSubPermissionsArgsError(t *testing.T) { + ns := "public/grant-sub-permissions-args-tests" + + args := []string{"grant-subscription-permission", ns} + _, _, _, err := TestNamespaceCommands(GrantSubPermissionsCmd, args) + assert.NotNil(t, err) + assert.Equal(t, "required flag(s) \"role\" not set", err.Error()) + + args = []string{"grant-subscription-permission", "--role", "test-role"} + _, _, nameErr, _ := TestNamespaceCommands(GrantSubPermissionsCmd, args) + assert.NotNil(t, nameErr) + assert.Equal(t, "need to specified namespace name and subscription name", nameErr.Error()) + + args = []string{"grant-subscription-permission", "--role", "test-role", ns} + _, execErr, _, _ := TestNamespaceCommands(GrantSubPermissionsCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "need to specified namespace name and subscription name", execErr.Error()) +} diff --git a/pkg/ctl/namespace/namespace.go b/pkg/ctl/namespace/namespace.go index dbab24fbb..91c495a4b 100644 --- a/pkg/ctl/namespace/namespace.go +++ b/pkg/ctl/namespace/namespace.go @@ -54,6 +54,11 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getReplicationClusters) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, unload) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, splitBundle) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetPermissionsCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GrantPermissionsCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RevokePermissionsCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GrantSubPermissionsCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RevokeSubPermissionsCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, ClearBacklogCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetDispatchRateCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetDispatchRateCmd) diff --git a/pkg/ctl/namespace/permissions.go b/pkg/ctl/namespace/permissions.go new file mode 100644 index 000000000..37c5ff4a2 --- /dev/null +++ b/pkg/ctl/namespace/permissions.go @@ -0,0 +1,80 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/streamnative/pulsarctl/pkg/cmdutils" + "github.com/streamnative/pulsarctl/pkg/pulsar" +) + +func GetPermissionsCmd(vc *cmdutils.VerbCmd) { + var desc pulsar.LongDescription + desc.CommandUsedFor = "This command is used for getting permissions configure data of a namespace." + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []pulsar.Example + getNs := pulsar.Example{ + Desc: "Get permissions configure data of a namespace (tenant)/(namespace)", + Command: "pulsarctl namespaces permissions (tenant)/(namespace)", + } + examples = append(examples, getNs) + desc.CommandExamples = examples + + var out []pulsar.Output + successOut := pulsar.Output{ + Desc: "normal output", + Out: "{\n" + + " \"\": [\n" + + " \"\"\n" + + " ]" + + "\n}", + } + out = append(out, successOut, ArgError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "permissions", + "Get permissions configure data of a namespace", + desc.ToString(), + desc.ExampleToString()) + + vc.SetRunFuncWithNameArg(func() error { + return doGetPermissions(vc) + }) +} + +func doGetPermissions(vc *cmdutils.VerbCmd) error { + // for testing + if vc.NameError != nil { + return vc.NameError + } + + ns, err := pulsar.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + data, err := admin.Namespaces().GetNamespacePermissions(*ns) + if err == nil { + cmdutils.PrintJSON(vc.Command.OutOrStdout(), data) + } + + return err +} diff --git a/pkg/ctl/namespace/permissions_test.go b/pkg/ctl/namespace/permissions_test.go new file mode 100644 index 000000000..4d37af997 --- /dev/null +++ b/pkg/ctl/namespace/permissions_test.go @@ -0,0 +1,56 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "encoding/json" + "testing" + + "github.com/streamnative/pulsarctl/pkg/pulsar" + + "github.com/stretchr/testify/assert" +) + +func TestPermissionsCmd(t *testing.T) { + ns := "public/test-permissions-ns" + + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"permissions", ns} + out, execErr, _, _ := TestNamespaceCommands(GetPermissionsCmd, args) + assert.Nil(t, execErr) + + var permissions map[string][]pulsar.AuthAction + err := json.Unmarshal(out.Bytes(), &permissions) + if err != nil { + t.Fatal(err) + } + + empty := make(map[string][]pulsar.AuthAction) + + assert.Equal(t, empty, permissions) +} + +func TestGetPermissionsArgsError(t *testing.T) { + args := []string{"permissions"} + _, _, nameErr, _ := TestNamespaceCommands(GetPermissionsCmd, args) + assert.NotNil(t, nameErr.Error()) + assert.Equal(t, "only one argument is allowed to be used as a name", nameErr.Error()) +} diff --git a/pkg/ctl/namespace/revoke_permission.go b/pkg/ctl/namespace/revoke_permission.go new file mode 100644 index 000000000..0b4decd3d --- /dev/null +++ b/pkg/ctl/namespace/revoke_permission.go @@ -0,0 +1,89 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/streamnative/pulsarctl/pkg/cmdutils" + "github.com/streamnative/pulsarctl/pkg/pulsar" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func RevokePermissionsCmd(vc *cmdutils.VerbCmd) { + var desc pulsar.LongDescription + desc.CommandUsedFor = "This command is used for revoking a client role permissions of accessing a namespace." + desc.CommandPermission = "This command requires tenant admin permissions and " + + "broker has read-writer permissions on the zookeeper." + + var examples []pulsar.Example + revoke := pulsar.Example{ + Desc: "Revoke the client role (role-name) of accessing the namespace (namespace-name)", + Command: "pulsarctl namespaces revoke-permission --role (role-name) (namespace-name)", + } + examples = append(examples, revoke) + desc.CommandExamples = examples + + var out []pulsar.Output + successOut := pulsar.Output{ + Desc: "normal output", + Out: "Revoke the client role (role-name) permissions of accessing the namespace (namespace-name) successfully", + } + out = append(out, successOut, ArgError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "revoke-permission", + "Revoke a client role permissions of accessing a namespace", + desc.ToString(), + desc.ExampleToString()) + + var role string + + vc.SetRunFuncWithNameArg(func() error { + return doRevokePermissions(vc, role) + }) + + vc.FlagSetGroup.InFlagSet("Revoke Permissions", func(set *pflag.FlagSet) { + set.StringVar(&role, "role", "", + "Client role to which revoke permissions") + cobra.MarkFlagRequired(set, "role") + }) +} + +func doRevokePermissions(vc *cmdutils.VerbCmd, role string) error { + // for testing + if vc.NameError != nil { + return vc.NameError + } + + ns, err := pulsar.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RevokeNamespacePermission(*ns, role) + if err == nil { + vc.Command.Printf("Revoke the client role %s permissions of accessing the namespace %s successfully", + role, ns.String()) + } + + return err +} diff --git a/pkg/ctl/namespace/revoke_permission_test.go b/pkg/ctl/namespace/revoke_permission_test.go new file mode 100644 index 000000000..dd222af32 --- /dev/null +++ b/pkg/ctl/namespace/revoke_permission_test.go @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRevokePermissionsArgsError(t *testing.T) { + ns := "public/revoke-permissions-args-tests" + + args := []string{"revoke-permission", ns} + _, _, _, err := TestNamespaceCommands(RevokePermissionsCmd, args) + assert.NotNil(t, err) + assert.Equal(t, "required flag(s) \"role\" not set", err.Error()) + + args = []string{"revoke-permission", "--role", "test-role"} + _, _, nameErr, _ := TestNamespaceCommands(RevokePermissionsCmd, args) + assert.NotNil(t, nameErr) + assert.Equal(t, "only one argument is allowed to be used as a name", nameErr.Error()) +} diff --git a/pkg/ctl/namespace/revoke_subscription_permission.go b/pkg/ctl/namespace/revoke_subscription_permission.go new file mode 100644 index 000000000..630787221 --- /dev/null +++ b/pkg/ctl/namespace/revoke_subscription_permission.go @@ -0,0 +1,105 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/streamnative/pulsarctl/pkg/cmdutils" + "github.com/streamnative/pulsarctl/pkg/pulsar" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func RevokeSubPermissionsCmd(vc *cmdutils.VerbCmd) { + var desc pulsar.LongDescription + desc.CommandUsedFor = "This command is used for revoking a client role permissions of " + + "accessing a subscription of a namespace." + desc.CommandPermission = "This command requires tenant admin permissions and " + + "broker has read-writer permissions on the zookeeper." + + var examples []pulsar.Example + revoke := pulsar.Example{ + Desc: "Revoke a client role (role-name) permissions of accessing the subscription " + + "(subscription-name) of the (namespace-name)", + Command: "pulsarctl namespaces revoke-subscription-permission --role (role-name) " + + "(namespace-name) (subscription-name)", + } + examples = append(examples, revoke) + desc.CommandExamples = examples + + var out []pulsar.Output + successOut := pulsar.Output{ + Desc: "normal output", + Out: "Revoke the client role (role-name) permissions of accessing the subscription " + + "(subscription-name) of the namespace (namespace-name) successfully", + } + + argsError := pulsar.Output{ + Desc: "the namespace name is not specified or the subscription name is not specified", + Out: "[✖] need to specified namespace name and subscription name", + } + out = append(out, successOut, argsError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "revoke-subscription-permission", + "Revoke a client role permissions of accessing a subscription of a namespace", + desc.ToString(), + desc.ToString()) + + var role string + + vc.SetRunFuncWithMultiNameArgs(func() error { + return doRevokeSubPermissions(vc, role) + }, func(args []string) error { + if len(args) != 2 { + return errors.New("need to specified namespace name and subscription name") + } + return nil + }) + + vc.FlagSetGroup.InFlagSet("Revoke Subscription Permissions", func(set *pflag.FlagSet) { + set.StringVar(&role, "role", "", + "Client role to which revoke permissions") + cobra.MarkFlagRequired(set, "role") + }) +} + +func doRevokeSubPermissions(vc *cmdutils.VerbCmd, role string) error { + // for testing + if vc.NameError != nil { + return vc.NameError + } + + ns, err := pulsar.GetNamespaceName(vc.NameArgs[0]) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RevokeSubPermission(*ns, vc.NameArgs[1], role) + if err == nil { + vc.Command.Printf("Revoke the client role %s permissions of accessing the "+ + "subscription %s of the namespace %s successfully\n", + role, vc.NameArgs[1], ns.String()) + } + + return err +} diff --git a/pkg/ctl/namespace/revoke_subscription_permission_test.go b/pkg/ctl/namespace/revoke_subscription_permission_test.go new file mode 100644 index 000000000..69583754d --- /dev/null +++ b/pkg/ctl/namespace/revoke_subscription_permission_test.go @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRevokeSubPermissionsArgsError(t *testing.T) { + ns := "public/revoke-sub-permissions-args-tests" + + args := []string{"revoke-subscription-permission", ns} + _, _, _, err := TestNamespaceCommands(RevokeSubPermissionsCmd, args) + assert.NotNil(t, err) + assert.Equal(t, "required flag(s) \"role\" not set", err.Error()) + + args = []string{"revoke-subscription-permission", "--role", "test-role"} + _, _, nameErr, _ := TestNamespaceCommands(RevokeSubPermissionsCmd, args) + assert.NotNil(t, nameErr) + assert.Equal(t, "need to specified namespace name and subscription name", nameErr.Error()) +} diff --git a/pkg/ctl/topic/permission/grant_test.go b/pkg/ctl/topic/permission/grant_test.go index 0e6723864..5748f1506 100644 --- a/pkg/ctl/topic/permission/grant_test.go +++ b/pkg/ctl/topic/permission/grant_test.go @@ -125,6 +125,6 @@ func TestGrantPermissionArgError(t *testing.T) { } _, execErr, _, _ = test.TestTopicCommands(GrantPermissionCmd, args) assert.NotNil(t, execErr) - assert.Equal(t, "The auth action only can be specified as 'produce', "+ + assert.Equal(t, "The auth action only can be specified as 'produce', "+ "'consume', or 'functions'. Invalid auth action 'args-error-action'", execErr.Error()) } diff --git a/pkg/pulsar/auth_action.go b/pkg/pulsar/auth_action.go index 1bf9fe1e6..84e42debb 100644 --- a/pkg/pulsar/auth_action.go +++ b/pkg/pulsar/auth_action.go @@ -36,7 +36,7 @@ func ParseAuthAction(action string) (AuthAction, error) { case "functions": return functionsAuth, nil default: - return "", errors.Errorf("The auth action only can be specified as 'produce', "+ + return "", errors.Errorf("The auth action only can be specified as 'produce', "+ "'consume', or 'functions'. Invalid auth action '%s'", action) } } diff --git a/pkg/pulsar/namespace.go b/pkg/pulsar/namespace.go index d1e29a2f5..20e88a48f 100644 --- a/pkg/pulsar/namespace.go +++ b/pkg/pulsar/namespace.go @@ -118,6 +118,12 @@ type Namespaces interface { // Split namespace bundle SplitNamespaceBundle(namespace, bundle string, unloadSplitBundles bool) error + GetNamespacePermissions(namespace NameSpaceName) (map[string][]AuthAction, error) + GrantNamespacePermission(namespace NameSpaceName, role string, action []AuthAction) error + RevokeNamespacePermission(namespace NameSpaceName, role string) error + GrantSubPermission(namespace NameSpaceName, sName string, roles []string) error + RevokeSubPermission(namespace NameSpaceName, sName, role string) error + // Set the given subscription auth mode on all topics on a namespace SetSubscriptionAuthMode(namespace NameSpaceName, mode SubscriptionAuthMode) error @@ -483,6 +489,39 @@ func (n *namespaces) SplitNamespaceBundle(namespace, bundle string, unloadSplitB return n.client.putWithQueryParams(endpoint, "", nil, params) } +func (n *namespaces) GetNamespacePermissions(namespace NameSpaceName) (map[string][]AuthAction, error) { + endpoint := n.client.endpoint(n.basePath, namespace.String(), "permissions") + var permissions map[string][]AuthAction + err := n.client.get(endpoint, &permissions) + return permissions, err +} + +func (n *namespaces) GrantNamespacePermission(namespace NameSpaceName, role string, action []AuthAction) error { + endpoint := n.client.endpoint(n.basePath, namespace.String(), "permissions", role) + s := make([]string, 0) + for _, v := range action { + s = append(s, v.String()) + } + return n.client.post(endpoint, s) +} + +func (n *namespaces) RevokeNamespacePermission(namespace NameSpaceName, role string) error { + endpoint := n.client.endpoint(n.basePath, namespace.String(), "permissions", role) + return n.client.delete(endpoint) +} + +func (n *namespaces) GrantSubPermission(namespace NameSpaceName, sName string, roles []string) error { + endpoint := n.client.endpoint(n.basePath, namespace.String(), "permissions", + "subscription", sName) + return n.client.post(endpoint, roles) +} + +func (n *namespaces) RevokeSubPermission(namespace NameSpaceName, sName, role string) error { + endpoint := n.client.endpoint(n.basePath, namespace.String(), "permissions", + "subscription", sName, role) + return n.client.delete(endpoint) +} + func (n *namespaces) SetSubscriptionAuthMode(namespace NameSpaceName, mode SubscriptionAuthMode) error { endpoint := n.client.endpoint(n.basePath, namespace.String(), "subscriptionAuthMode") return n.client.post(endpoint, mode.String())