| // Copyright 2018 The LUCI Authors. |
| // |
| // 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 repo |
| |
| import ( |
| "context" |
| "strings" |
| |
| "go.chromium.org/luci/auth/identity" |
| "go.chromium.org/luci/common/data/stringset" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/server/auth" |
| |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| ) |
| |
| // impliedRoles defines what roles are "inherited" by other roles, e.g. |
| // WRITERs are automatically READERs, so hasRole(..., READER) should return true |
| // for WRITERs too. |
| // |
| // The format is "role -> {role itself} + set of roles implied by it, perhaps |
| // indirectly". |
| // |
| // If a role is missing from this map, it assumed to not be implying any roles. |
| var impliedRoles = map[api.Role][]api.Role{ |
| api.Role_READER: {api.Role_READER}, |
| api.Role_WRITER: {api.Role_WRITER, api.Role_READER}, |
| api.Role_OWNER: {api.Role_OWNER, api.Role_WRITER, api.Role_READER}, |
| } |
| |
| // impliedRolesRev is reverse of impliedRoles mapping. |
| // |
| // The format is "role -> {role itself} + set of roles that inherit it, perhaps |
| // indirectly". |
| // |
| // If a role is missing from this map, it assumed to not be inherited by |
| // anything. |
| var impliedRolesRev = map[api.Role]map[api.Role]struct{}{ |
| api.Role_READER: roleSet(api.Role_READER, api.Role_WRITER, api.Role_OWNER), |
| api.Role_WRITER: roleSet(api.Role_WRITER, api.Role_OWNER), |
| api.Role_OWNER: roleSet(api.Role_OWNER), |
| } |
| |
| func roleSet(roles ...api.Role) map[api.Role]struct{} { |
| m := make(map[api.Role]struct{}, len(roles)) |
| for _, r := range roles { |
| m[r] = struct{}{} |
| } |
| return m |
| } |
| |
| // hasRole checks whether the current caller has the given role in any of the |
| // supplied PrefixMetadata objects. |
| // |
| // It understands the role inheritance defined by impliedRoles map. |
| // |
| // 'metas' is metadata for some prefix and all parent prefixes. It is expected |
| // to be ordered by the prefix length (shortest first). Ordering is not really |
| // used now, but it may change in the future. |
| // |
| // Returns only transient errors. |
| func hasRole(ctx context.Context, metas []*api.PrefixMetadata, role api.Role) (bool, error) { |
| caller := string(auth.CurrentIdentity(ctx)) // e.g. "user:abc@example.com" |
| |
| // E.g. if 'role' is READER, 'roles' will be {READER, WRITER, OWNER}. |
| roles := impliedRolesRev[role] |
| if roles == nil { |
| roles = roleSet(role) |
| } |
| |
| // Enumerate the set of principals that have any of the requested roles in any |
| // of the prefixes. Exit early if hitting the direct match, otherwise proceed |
| // to more expensive group membership checks. Note that we don't use isInACL |
| // here because we want to postpone all group checks until the very end, |
| // checking memberships in all groups mentioned in 'metas' at once. |
| groups := stringset.New(10) // 10 is picked arbitrarily |
| for _, meta := range metas { |
| for _, acl := range meta.Acls { |
| if _, ok := roles[acl.Role]; !ok { |
| continue // not the role we are interested in |
| } |
| for _, p := range acl.Principals { |
| if p == caller { |
| return true, nil // the caller was specified in ACLs explicitly |
| } |
| // Is this a reference to a group? |
| if s := strings.SplitN(p, ":", 2); len(s) == 2 && s[0] == "group" { |
| groups.Add(s[1]) |
| } |
| } |
| } |
| } |
| |
| yes, err := auth.IsMember(ctx, groups.ToSlice()...) |
| if err != nil { |
| return false, errors.Annotate(err, "failed to check group memberships when checking ACLs for role %s", role).Err() |
| } |
| return yes, nil |
| } |
| |
| // rolesInPrefix returns a union of roles `ident` has in given supplied |
| // PrefixMetadata objects. |
| // |
| // It understands the role inheritance defined by impliedRoles map. |
| // |
| // Returns only transient errors. |
| func rolesInPrefix(ctx context.Context, ident identity.Identity, metas []*api.PrefixMetadata) ([]api.Role, error) { |
| roles := roleSet() |
| for _, meta := range metas { |
| for _, acl := range meta.Acls { |
| if _, ok := roles[acl.Role]; ok { |
| continue // seen this role already |
| } |
| switch yes, err := isInACL(ctx, ident, acl); { |
| case err != nil: |
| return nil, err |
| case yes: |
| // Add acl.Role and all roles implied by it to 'roles' set. |
| for _, r := range impliedRoles[acl.Role] { |
| roles[r] = struct{}{} |
| } |
| } |
| } |
| } |
| |
| // Arrange the result in the order of Role enum definition. |
| out := make([]api.Role, 0, len(roles)) |
| for r := api.Role_READER; r <= api.Role_OWNER; r++ { |
| if _, ok := roles[r]; ok { |
| out = append(out, r) |
| } |
| } |
| return out, nil |
| } |
| |
| // isInACL is true if `ident` is in the given access control list. |
| // |
| // Most callers will use auth.CurrentIdentity() for this value. |
| func isInACL(ctx context.Context, ident identity.Identity, acl *api.PrefixMetadata_ACL) (bool, error) { |
| var groups []string |
| for _, p := range acl.Principals { |
| if p == string(ident) { |
| return true, nil // the identity was specified in ACLs explicitly |
| } |
| if s := strings.SplitN(p, ":", 2); len(s) == 2 && s[0] == "group" { |
| groups = append(groups, s[1]) |
| } |
| } |
| |
| // We don't use auth.IsMember because we want to check for `ident` rather than |
| // for the current caller. |
| var yes bool |
| var err error |
| if s := auth.GetState(ctx); s != nil { |
| yes, err = s.DB().IsMember(ctx, ident, groups) |
| } else { |
| err = auth.ErrNotConfigured |
| } |
| if err != nil { |
| return false, errors.Annotate(err, "failed to check group memberships when checking ACLs").Err() |
| } |
| return yes, nil |
| } |