forked from rancher/rancher
-
Notifications
You must be signed in to change notification settings - Fork 0
/
validator_multiclusterapp.go
203 lines (195 loc) · 7.78 KB
/
validator_multiclusterapp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package multiclusterapp
import (
"fmt"
"net/http"
"reflect"
"sort"
"strings"
"time"
"github.com/rancher/norman/api/access"
"github.com/rancher/norman/types"
"github.com/rancher/norman/types/convert"
"github.com/rancher/norman/types/set"
"github.com/rancher/norman/types/values"
gaccess "github.com/rancher/rancher/pkg/api/customization/globalnamespaceaccess"
"github.com/rancher/rancher/pkg/ref"
"github.com/rancher/types/apis/management.cattle.io/v3"
managementschema "github.com/rancher/types/apis/management.cattle.io/v3/schema"
pv3 "github.com/rancher/types/apis/project.cattle.io/v3"
"github.com/rancher/types/client/management/v3"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
)
type Wrapper struct {
MultiClusterApps v3.MultiClusterAppInterface
MultiClusterAppLister v3.MultiClusterAppLister
MultiClusterAppRevisionLister v3.MultiClusterAppRevisionLister
PrtbLister v3.ProjectRoleTemplateBindingLister
CrtbLister v3.ClusterRoleTemplateBindingLister
RoleTemplateLister v3.RoleTemplateLister
Users v3.UserInterface
GrbLister v3.GlobalRoleBindingLister
GrLister v3.GlobalRoleLister
Prtbs v3.ProjectRoleTemplateBindingInterface
Crtbs v3.ClusterRoleTemplateBindingInterface
ProjectLister v3.ProjectLister
ClusterLister v3.ClusterLister
Apps pv3.AppInterface
}
const (
mcAppLabel = "io.cattle.field/multiClusterAppId"
creatorIDAnn = "field.cattle.io/creatorId"
)
func (w Wrapper) LinkHandler(apiContext *types.APIContext, next types.RequestHandler) error {
switch apiContext.Link {
case "revisions":
var app client.MultiClusterApp
if err := access.ByID(apiContext, &managementschema.Version, client.MultiClusterAppType, apiContext.ID, &app); err != nil {
return err
}
var appRevisions, filtered []map[string]interface{}
if err := access.List(apiContext, &managementschema.Version, client.MultiClusterAppRevisionType, &types.QueryOptions{}, &appRevisions); err != nil {
return err
}
for _, revision := range appRevisions {
labels := convert.ToMapInterface(revision["labels"])
if reflect.DeepEqual(labels[mcAppLabel], app.Name) {
filtered = append(filtered, revision)
}
}
sort.SliceStable(filtered, func(i, j int) bool {
val1, err := time.Parse(time.RFC3339, convert.ToString(values.GetValueN(filtered[i], "created")))
if err != nil {
logrus.Infof("error parsing time %v", err)
}
val2, err := time.Parse(time.RFC3339, convert.ToString(values.GetValueN(filtered[j], "created")))
if err != nil {
logrus.Infof("error parsing time %v", err)
}
return val1.After(val2)
})
apiContext.Type = client.MultiClusterAppRevisionType
apiContext.WriteResponse(http.StatusOK, filtered)
return nil
}
return nil
}
func (w Wrapper) Validator(request *types.APIContext, schema *types.Schema, data map[string]interface{}) error {
if request.Method != http.MethodPut && request.Method != http.MethodPost {
return nil
}
ma := gaccess.MemberAccess{
Users: w.Users,
PrtbLister: w.PrtbLister,
CrtbLister: w.CrtbLister,
RoleTemplateLister: w.RoleTemplateLister,
GrbLister: w.GrbLister,
GrLister: w.GrLister,
Prtbs: w.Prtbs,
Crtbs: w.Crtbs,
ProjectLister: w.ProjectLister,
ClusterLister: w.ClusterLister,
}
var targetProjects []string
callerID := request.Request.Header.Get(gaccess.ImpersonateUserHeader)
if request.Method == http.MethodPost {
// create request, caller is owner/creator
// Request is POST, hence multiclusterapp is being created.
// check if creator has the given roles in all target projects
targets, _ := values.GetSlice(data, client.MultiClusterAppFieldTargets)
for _, t := range targets {
targetProjects = append(targetProjects, convert.ToString(t[client.TargetFieldProjectID]))
}
roleTemplates := convert.ToStringSlice(data[client.MultiClusterAppFieldRoles])
return ma.EnsureRoleInTargets(targetProjects, roleTemplates, callerID)
}
// edit request, only editing members, roles and answers is allowed through this
split := strings.SplitN(request.ID, ":", 2)
if len(split) != 2 {
return fmt.Errorf("incorrect multiclusterapp ID %v", request.ID)
}
mcapp, err := w.MultiClusterApps.GetNamespaced(split[0], split[1], v1.GetOptions{})
if err != nil {
return err
}
metaAccessor, err := meta.Accessor(mcapp)
if err != nil {
return err
}
creatorID, ok := metaAccessor.GetAnnotations()[creatorIDAnn]
if !ok {
return fmt.Errorf("MultiClusterApp %v has no creatorId annotation", metaAccessor.GetName())
}
accessType, err := ma.GetAccessTypeOfCaller(callerID, creatorID, mcapp.Name, mcapp.Spec.Members)
if err != nil {
return err
}
if accessType == gaccess.ReadonlyAccess {
return fmt.Errorf("read-only members cannot update multiclusterapp")
}
ownerAccess := accessType == gaccess.OwnerAccess
// only members and roles list, and templateversion/answers can be edited through PUT, for updating target projects, we need to use actions only
// that's why target projects field has been made non updatable in rancher/types
if err := gaccess.CheckAccessToUpdateMembers(mcapp.Spec.Members, data, ownerAccess); err != nil {
return err
}
// check whether roles are being edited, if yes then only owner should be allowed to, and should have this role in all projects
roles := convert.ToStringSlice(data[client.MultiClusterAppFieldRoles])
newRoles := make(map[string]bool)
for _, r := range roles {
newRoles[r] = true
}
originalRoles := make(map[string]bool)
for _, r := range mcapp.Spec.Roles {
originalRoles[r] = true
}
// get difference between new and original roles
rolesToAdd, rolesToRemove, _ := set.Diff(newRoles, originalRoles)
if len(rolesToAdd) == 0 && len(rolesToRemove) == 0 {
// this UPDATE request is not modifying the multiclusterapp roles.
// So return without calling EnsureRoleInTargets, because this UPDATE could be called by a mcapp member with access type member,
// just to update the templateversion or answers; this member is allowed to not have all roles in all targets
return nil
}
if (len(rolesToAdd) != 0 || len(rolesToRemove) != 0) && !ownerAccess {
return fmt.Errorf("user %v is not an owner of multiclusterapp %v, cannot edit roles", callerID, mcapp.Name)
}
// check if modifier has all roles listed in toAdd in all projects
for _, t := range mcapp.Spec.Targets {
targetProjects = append(targetProjects, t.ProjectName)
}
if err = ma.EnsureRoleInTargets(targetProjects, rolesToAdd, callerID); err != nil {
return err
}
// at this point we know the roles have changed, and that the user has the new roles in targets,
// retry underlying app upgrade
for _, t := range mcapp.Spec.Targets {
_, projectNS := ref.Parse(t.ProjectName)
if t.AppName != "" {
err := wait.ExponentialBackoff(backoff, func() (bool, error) {
app, err := w.Apps.GetNamespaced(projectNS, t.AppName, v1.GetOptions{})
if err != nil || app == nil {
return false, fmt.Errorf("error %v getting app %s in %s", err, t.AppName, projectNS)
}
if val, ok := app.Labels["mcapp"]; !ok || val != mcapp.Name {
return false, fmt.Errorf("app %s in %s missing multi cluster app label", t.AppName, projectNS)
}
pv3.AppConditionUserTriggeredAction.True(app)
if _, err := w.Apps.Update(app); err != nil {
if apierrors.IsConflict(err) {
return false, nil
}
return false, err
}
return true, nil
})
if err != nil {
return err
}
}
}
return ma.RemoveRolesFromTargets(targetProjects, rolesToRemove, mcapp.Name, false)
}