1
16
17 package auth
18
19 import (
20 "errors"
21 "fmt"
22
23 "github.com/spf13/cobra"
24 "k8s.io/apimachinery/pkg/runtime"
25 "k8s.io/klog/v2"
26
27 rbacv1 "k8s.io/api/rbac/v1"
28 rbacv1alpha1 "k8s.io/api/rbac/v1alpha1"
29 rbacv1beta1 "k8s.io/api/rbac/v1beta1"
30 "k8s.io/cli-runtime/pkg/genericclioptions"
31 "k8s.io/cli-runtime/pkg/genericiooptions"
32 "k8s.io/cli-runtime/pkg/printers"
33 "k8s.io/cli-runtime/pkg/resource"
34 corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
35 rbacv1client "k8s.io/client-go/kubernetes/typed/rbac/v1"
36 "k8s.io/component-helpers/auth/rbac/reconciliation"
37 cmdutil "k8s.io/kubectl/pkg/cmd/util"
38 "k8s.io/kubectl/pkg/scheme"
39 "k8s.io/kubectl/pkg/util/templates"
40 )
41
42
43
44 type ReconcileOptions struct {
45 PrintFlags *genericclioptions.PrintFlags
46 FilenameOptions *resource.FilenameOptions
47 DryRun bool
48 RemoveExtraPermissions bool
49 RemoveExtraSubjects bool
50
51 Visitor resource.Visitor
52 RBACClient rbacv1client.RbacV1Interface
53 NamespaceClient corev1client.CoreV1Interface
54
55 PrintObject printers.ResourcePrinterFunc
56
57 genericiooptions.IOStreams
58 }
59
60 var (
61 reconcileLong = templates.LongDesc(`
62 Reconciles rules for RBAC role, role binding, cluster role, and cluster role binding objects.
63
64 Missing objects are created, and the containing namespace is created for namespaced objects, if required.
65
66 Existing roles are updated to include the permissions in the input objects,
67 and remove extra permissions if --remove-extra-permissions is specified.
68
69 Existing bindings are updated to include the subjects in the input objects,
70 and remove extra subjects if --remove-extra-subjects is specified.
71
72 This is preferred to 'apply' for RBAC resources so that semantically-aware merging of rules and subjects is done.`)
73
74 reconcileExample = templates.Examples(`
75 # Reconcile RBAC resources from a file
76 kubectl auth reconcile -f my-rbac-rules.yaml`)
77 )
78
79
80 func NewReconcileOptions(ioStreams genericiooptions.IOStreams) *ReconcileOptions {
81 return &ReconcileOptions{
82 FilenameOptions: &resource.FilenameOptions{},
83 PrintFlags: genericclioptions.NewPrintFlags("reconciled").WithTypeSetter(scheme.Scheme),
84 IOStreams: ioStreams,
85 }
86 }
87
88
89 func NewCmdReconcile(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
90 o := NewReconcileOptions(streams)
91
92 cmd := &cobra.Command{
93 Use: "reconcile -f FILENAME",
94 DisableFlagsInUseLine: true,
95 Short: "Reconciles rules for RBAC role, role binding, cluster role, and cluster role binding objects",
96 Long: reconcileLong,
97 Example: reconcileExample,
98 Run: func(cmd *cobra.Command, args []string) {
99 cmdutil.CheckErr(o.Complete(cmd, f, args))
100 cmdutil.CheckErr(o.Validate())
101 cmdutil.CheckErr(o.RunReconcile())
102 },
103 }
104
105 o.PrintFlags.AddFlags(cmd)
106
107 cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to reconcile.")
108 cmd.Flags().BoolVar(&o.RemoveExtraPermissions, "remove-extra-permissions", o.RemoveExtraPermissions, "If true, removes extra permissions added to roles")
109 cmd.Flags().BoolVar(&o.RemoveExtraSubjects, "remove-extra-subjects", o.RemoveExtraSubjects, "If true, removes extra subjects added to rolebindings")
110 cmdutil.AddDryRunFlag(cmd)
111
112 return cmd
113 }
114
115
116 func (o *ReconcileOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string) error {
117 if err := o.FilenameOptions.RequireFilenameOrKustomize(); err != nil {
118 return err
119 }
120
121 if len(args) > 0 {
122 return errors.New("no arguments are allowed")
123 }
124
125 dryRun, err := getClientSideDryRun(cmd)
126 if err != nil {
127 return err
128 }
129 o.DryRun = dryRun
130
131 namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
132 if err != nil {
133 return err
134 }
135
136 r := f.NewBuilder().
137 WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
138 ContinueOnError().
139 NamespaceParam(namespace).DefaultNamespace().
140 FilenameParam(enforceNamespace, o.FilenameOptions).
141 Flatten().
142 Local().
143 Do()
144
145 if err := r.Err(); err != nil {
146 return err
147 }
148 o.Visitor = r
149
150 clientConfig, err := f.ToRESTConfig()
151 if err != nil {
152 return err
153 }
154 o.RBACClient, err = rbacv1client.NewForConfig(clientConfig)
155 if err != nil {
156 return err
157 }
158 o.NamespaceClient, err = corev1client.NewForConfig(clientConfig)
159 if err != nil {
160 return err
161 }
162
163 if o.DryRun {
164 o.PrintFlags.Complete("%s (dry run)")
165 }
166 printer, err := o.PrintFlags.ToPrinter()
167 if err != nil {
168 return err
169 }
170
171 o.PrintObject = printer.PrintObj
172 return nil
173 }
174
175
176 func (o *ReconcileOptions) Validate() error {
177 if o.Visitor == nil {
178 return errors.New("ReconcileOptions.Visitor must be set")
179 }
180 if o.RBACClient == nil {
181 return errors.New("ReconcileOptions.RBACClient must be set")
182 }
183 if o.NamespaceClient == nil {
184 return errors.New("ReconcileOptions.NamespaceClient must be set")
185 }
186 if o.PrintObject == nil {
187 return errors.New("ReconcileOptions.Print must be set")
188 }
189 if o.Out == nil {
190 return errors.New("ReconcileOptions.Out must be set")
191 }
192 if o.ErrOut == nil {
193 return errors.New("ReconcileOptions.Err must be set")
194 }
195 return nil
196 }
197
198
199 func (o *ReconcileOptions) RunReconcile() error {
200 return o.Visitor.Visit(func(info *resource.Info, err error) error {
201 if err != nil {
202 return err
203 }
204
205 switch t := info.Object.(type) {
206 case *rbacv1.Role:
207 reconcileOptions := reconciliation.ReconcileRoleOptions{
208 Confirm: !o.DryRun,
209 RemoveExtraPermissions: o.RemoveExtraPermissions,
210 Role: reconciliation.RoleRuleOwner{Role: t},
211 Client: reconciliation.RoleModifier{
212 NamespaceClient: o.NamespaceClient.Namespaces(),
213 Client: o.RBACClient,
214 },
215 }
216 result, err := reconcileOptions.Run()
217 if err != nil {
218 return err
219 }
220 o.printResults(result.Role.GetObject(), nil, nil, result.MissingRules, result.ExtraRules, result.Operation, result.Protected)
221
222 case *rbacv1.ClusterRole:
223 reconcileOptions := reconciliation.ReconcileRoleOptions{
224 Confirm: !o.DryRun,
225 RemoveExtraPermissions: o.RemoveExtraPermissions,
226 Role: reconciliation.ClusterRoleRuleOwner{ClusterRole: t},
227 Client: reconciliation.ClusterRoleModifier{
228 Client: o.RBACClient.ClusterRoles(),
229 },
230 }
231 result, err := reconcileOptions.Run()
232 if err != nil {
233 return err
234 }
235 o.printResults(result.Role.GetObject(), nil, nil, result.MissingRules, result.ExtraRules, result.Operation, result.Protected)
236
237 case *rbacv1.RoleBinding:
238 reconcileOptions := reconciliation.ReconcileRoleBindingOptions{
239 Confirm: !o.DryRun,
240 RemoveExtraSubjects: o.RemoveExtraSubjects,
241 RoleBinding: reconciliation.RoleBindingAdapter{RoleBinding: t},
242 Client: reconciliation.RoleBindingClientAdapter{
243 Client: o.RBACClient,
244 NamespaceClient: o.NamespaceClient.Namespaces(),
245 },
246 }
247 result, err := reconcileOptions.Run()
248 if err != nil {
249 return err
250 }
251 o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected)
252
253 case *rbacv1.ClusterRoleBinding:
254 reconcileOptions := reconciliation.ReconcileRoleBindingOptions{
255 Confirm: !o.DryRun,
256 RemoveExtraSubjects: o.RemoveExtraSubjects,
257 RoleBinding: reconciliation.ClusterRoleBindingAdapter{ClusterRoleBinding: t},
258 Client: reconciliation.ClusterRoleBindingClientAdapter{
259 Client: o.RBACClient.ClusterRoleBindings(),
260 },
261 }
262 result, err := reconcileOptions.Run()
263 if err != nil {
264 return err
265 }
266 o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected)
267
268 case *rbacv1beta1.Role,
269 *rbacv1beta1.RoleBinding,
270 *rbacv1beta1.ClusterRole,
271 *rbacv1beta1.ClusterRoleBinding,
272 *rbacv1alpha1.Role,
273 *rbacv1alpha1.RoleBinding,
274 *rbacv1alpha1.ClusterRole,
275 *rbacv1alpha1.ClusterRoleBinding:
276 return fmt.Errorf("only rbac.authorization.k8s.io/v1 is supported: not %T", t)
277
278 default:
279 klog.V(1).Infof("skipping %#v", info.Object.GetObjectKind())
280
281 }
282
283 return nil
284 })
285 }
286
287 func (o *ReconcileOptions) printResults(object runtime.Object,
288 missingSubjects, extraSubjects []rbacv1.Subject,
289 missingRules, extraRules []rbacv1.PolicyRule,
290 operation reconciliation.ReconcileOperation,
291 protected bool) {
292
293 o.PrintObject(object, o.Out)
294
295 caveat := ""
296 if protected {
297 caveat = ", but object opted out (rbac.authorization.kubernetes.io/autoupdate: false)"
298 }
299 switch operation {
300 case reconciliation.ReconcileNone:
301 return
302 case reconciliation.ReconcileCreate:
303 fmt.Fprintf(o.ErrOut, "\treconciliation required create%s\n", caveat)
304 case reconciliation.ReconcileUpdate:
305 fmt.Fprintf(o.ErrOut, "\treconciliation required update%s\n", caveat)
306 case reconciliation.ReconcileRecreate:
307 fmt.Fprintf(o.ErrOut, "\treconciliation required recreate%s\n", caveat)
308 }
309
310 if len(missingSubjects) > 0 {
311 fmt.Fprintf(o.ErrOut, "\tmissing subjects added:\n")
312 for _, s := range missingSubjects {
313 fmt.Fprintf(o.ErrOut, "\t\t%+v\n", s)
314 }
315 }
316 if o.RemoveExtraSubjects {
317 if len(extraSubjects) > 0 {
318 fmt.Fprintf(o.ErrOut, "\textra subjects removed:\n")
319 for _, s := range extraSubjects {
320 fmt.Fprintf(o.ErrOut, "\t\t%+v\n", s)
321 }
322 }
323 }
324 if len(missingRules) > 0 {
325 fmt.Fprintf(o.ErrOut, "\tmissing rules added:\n")
326 for _, r := range missingRules {
327 fmt.Fprintf(o.ErrOut, "\t\t%+v\n", r)
328 }
329 }
330 if o.RemoveExtraPermissions {
331 if len(extraRules) > 0 {
332 fmt.Fprintf(o.ErrOut, "\textra rules removed:\n")
333 for _, r := range extraRules {
334 fmt.Fprintf(o.ErrOut, "\t\t%+v\n", r)
335 }
336 }
337 }
338 }
339
340 func getClientSideDryRun(cmd *cobra.Command) (bool, error) {
341 dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd)
342 if err != nil {
343 return false, fmt.Errorf("error accessing --dry-run flag for command %s: %v", cmd.Name(), err)
344 }
345 if dryRunStrategy == cmdutil.DryRunServer {
346 return false, fmt.Errorf("--dry-run=server for command %s is not supported yet", cmd.Name())
347 }
348 return dryRunStrategy == cmdutil.DryRunClient, nil
349 }
350
View as plain text