| OLD | NEW |
| 1 // Copyright 2017 The LUCI Authors. | 1 // Copyright 2017 The LUCI Authors. |
| 2 // | 2 // |
| 3 // Licensed under the Apache License, Version 2.0 (the "License"); | 3 // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 // you may not use this file except in compliance with the License. | 4 // you may not use this file except in compliance with the License. |
| 5 // You may obtain a copy of the License at | 5 // You may obtain a copy of the License at |
| 6 // | 6 // |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 | 7 // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 // | 8 // |
| 9 // Unless required by applicable law or agreed to in writing, software | 9 // Unless required by applicable law or agreed to in writing, software |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, | 10 // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 // See the License for the specific language governing permissions and | 12 // See the License for the specific language governing permissions and |
| 13 // limitations under the License. | 13 // limitations under the License. |
| 14 | 14 |
| 15 package acl | 15 package acl |
| 16 | 16 |
| 17 import ( | 17 import ( |
| 18 » "golang.org/x/net/context" | 18 » "context" |
| 19 » "fmt" |
| 20 » "regexp" |
| 21 » "sort" |
| 22 » "strings" |
| 19 | 23 |
| 24 "github.com/luci/luci-go/common/data/stringset" |
| 25 "github.com/luci/luci-go/common/errors" |
| 26 "github.com/luci/luci-go/common/retry/transient" |
| 27 "github.com/luci/luci-go/scheduler/appengine/messages" |
| 20 "github.com/luci/luci-go/server/auth" | 28 "github.com/luci/luci-go/server/auth" |
| 29 "github.com/luci/luci-go/server/auth/identity" |
| 21 ) | 30 ) |
| 22 | 31 |
| 23 func IsJobOwner(c context.Context, projectID, jobName string) bool { | 32 // GrantsByRole can answer questions who can READ and who OWNS the task. |
| 24 » // TODO(vadimsh): Do real ACLs. | 33 type GrantsByRole struct { |
| 25 » ok, err := auth.IsMember(c, "administrators") | 34 » Owners []string `gae:",noindex"` |
| 26 » if err != nil { | 35 » Readers []string `gae:",noindex"` |
| 27 » » panic(err) | 36 } |
| 37 |
| 38 func (g *GrantsByRole) IsOwner(c context.Context) (bool, error) { |
| 39 » return hasGrant(c, g.Owners, groupsAdministrators) |
| 40 } |
| 41 |
| 42 func (g *GrantsByRole) IsReader(c context.Context) (bool, error) { |
| 43 » if len(g.Readers) == 0 && len(g.Owners) == 0 { |
| 44 » » // This is here for backwards compatiblity before ACLs were intr
oduced. |
| 45 » » // If Job doesn't specify READERs nor OWNERS explicitely, everyb
ody can read. |
| 46 » » // TODO(tAndrii): remove once every Job/Trigger has ACLs specifi
ed. |
| 47 » » return true, nil |
| 28 } | 48 } |
| 29 » return ok | 49 » return hasGrant(c, g.Owners, g.Readers, groupsAdministrators) |
| 30 } | 50 } |
| 51 |
| 52 func (g *GrantsByRole) Equal(o *GrantsByRole) bool { |
| 53 eqSlice := func(a, b []string) bool { |
| 54 if len(a) != len(b) { |
| 55 return false |
| 56 } |
| 57 for i := range a { |
| 58 if a[i] != b[i] { |
| 59 return false |
| 60 } |
| 61 } |
| 62 return true |
| 63 } |
| 64 return eqSlice(g.Owners, o.Owners) && eqSlice(g.Readers, o.Readers) |
| 65 } |
| 66 |
| 67 // AclSets are parsed and indexed `AclSet` of a project. |
| 68 type AclSets map[string][]*messages.Acl |
| 69 |
| 70 // ValidateAclSets validates list of AclSet of a project and returns AclSets. |
| 71 func ValidateAclSets(sets []*messages.AclSet) (AclSets, error) { |
| 72 as := make(AclSets, len(sets)) |
| 73 for _, s := range sets { |
| 74 if s.Name == "" { |
| 75 return nil, fmt.Errorf("missing 'name' field'") |
| 76 } |
| 77 if !aclSetNameRe.MatchString(s.Name) { |
| 78 return nil, fmt.Errorf("%q is not valid value for 'name'
field", s.Name) |
| 79 } |
| 80 if _, isDup := as[s.Name]; isDup { |
| 81 return nil, fmt.Errorf("aclSet name %q is not unique", s
.Name) |
| 82 } |
| 83 if len(s.GetAcls()) == 0 { |
| 84 return nil, fmt.Errorf("aclSet %q has no entries", s.Nam
e) |
| 85 } |
| 86 as[s.Name] = s.GetAcls() |
| 87 } |
| 88 return as, nil |
| 89 } |
| 90 |
| 91 // ValidateTaskAcls validates task's ACLs and returns TaskAcls. |
| 92 func ValidateTaskAcls(pSets AclSets, tSets []string, tAcls []*messages.Acl) (*Gr
antsByRole, error) { |
| 93 grantsLists := make([][]*messages.Acl, 0, 1+len(tSets)) |
| 94 if err := validateGrants(tAcls); err != nil { |
| 95 return nil, err |
| 96 } |
| 97 grantsLists = append(grantsLists, tAcls) |
| 98 for _, set := range tSets { |
| 99 grantsList, exists := pSets[set] |
| 100 if !exists { |
| 101 return nil, fmt.Errorf("referencing AclSet '%s' which do
esn't exist", set) |
| 102 } |
| 103 grantsLists = append(grantsLists, grantsList) |
| 104 } |
| 105 mg := mergeGrants(grantsLists...) |
| 106 if n := len(mg.Owners) + len(mg.Readers); n > maxGrantsPerJob { |
| 107 return nil, fmt.Errorf("Job or Trigger can have at most %d acls,
but %d given", maxGrantsPerJob, n) |
| 108 } |
| 109 return mg, nil |
| 110 } |
| 111 |
| 112 //////////////////////////////////////////////////////////////////////////////// |
| 113 |
| 114 var ( |
| 115 // aclSetNameRe is used to validate AclSet Name field. |
| 116 aclSetNameRe = regexp.MustCompile(`^[0-9A-Za-z_\-\.]{1,100}$`) |
| 117 // maxGrantsPerJob is how many different grants are specified for a job. |
| 118 maxGrantsPerJob = 32 |
| 119 |
| 120 groupsAdministrators = []string{"group:administrators"} |
| 121 ) |
| 122 |
| 123 func validateGrants(gs []*messages.Acl) error { |
| 124 for _, g := range gs { |
| 125 switch { |
| 126 case g.GetRole() != messages.Acl_OWNER && g.GetRole() != message
s.Acl_READER: |
| 127 return fmt.Errorf("invalid role %q", g.GetRole()) |
| 128 case g.GetGrantedTo() == "": |
| 129 return fmt.Errorf("missing granted_to for role %s", g.Ge
tRole()) |
| 130 case strings.HasPrefix(g.GetGrantedTo(), "group:"): |
| 131 if g.GetGrantedTo()[len("group:"):] == "" { |
| 132 return fmt.Errorf("invalid granted_to %q for rol
e %s: needs a group name", g.GetGrantedTo(), g.GetRole()) |
| 133 } |
| 134 default: |
| 135 id := g.GetGrantedTo() |
| 136 if !strings.ContainsRune(g.GetGrantedTo(), ':') { |
| 137 id = "user:" + g.GetGrantedTo() |
| 138 } |
| 139 if _, err := identity.MakeIdentity(id); err != nil { |
| 140 return errors.Annotate(err, "invalid granted_to
%q for role %s", g.GetGrantedTo(), g.GetRole()).Err() |
| 141 } |
| 142 } |
| 143 } |
| 144 return nil |
| 145 } |
| 146 |
| 147 // mergeGrants merges valid grants into GrantsByRole, removing and sorting dupli
cates. |
| 148 func mergeGrants(grantsLists ...[]*messages.Acl) *GrantsByRole { |
| 149 all := map[messages.Acl_Role]stringset.Set{ |
| 150 messages.Acl_OWNER: stringset.New(maxGrantsPerJob), |
| 151 messages.Acl_READER: stringset.New(maxGrantsPerJob), |
| 152 } |
| 153 for _, grantsList := range grantsLists { |
| 154 for _, g := range grantsList { |
| 155 all[g.GetRole()].Add(g.GetGrantedTo()) |
| 156 } |
| 157 } |
| 158 sortedSlice := func(s stringset.Set) []string { |
| 159 r := s.ToSlice() |
| 160 sort.Strings(r) |
| 161 return r |
| 162 } |
| 163 return &GrantsByRole{ |
| 164 Owners: sortedSlice(all[messages.Acl_OWNER]), |
| 165 Readers: sortedSlice(all[messages.Acl_READER]), |
| 166 } |
| 167 } |
| 168 |
| 169 // hasGrant is current user is covered by any given grants. |
| 170 func hasGrant(c context.Context, grantsList ...[]string) (bool, error) { |
| 171 currentIdentity := auth.CurrentIdentity(c) |
| 172 groups := []string{} |
| 173 for _, grants := range grantsList { |
| 174 for _, grant := range grants { |
| 175 if strings.HasPrefix(grant, "group:") { |
| 176 groups = append(groups, grant[len("group:"):]) |
| 177 continue |
| 178 } |
| 179 grantedIdentity := identity.Identity(grant) |
| 180 if !strings.ContainsRune(grant, ':') { |
| 181 // Just email. |
| 182 grantedIdentity = identity.Identity("user:" + gr
ant) |
| 183 } |
| 184 if grantedIdentity == currentIdentity { |
| 185 return true, nil |
| 186 } |
| 187 } |
| 188 } |
| 189 if isMember, err := auth.IsMember(c, groups...); err != nil { |
| 190 return false, transient.Tag.Apply(err) |
| 191 } else { |
| 192 return isMember, nil |
| 193 } |
| 194 } |
| OLD | NEW |