blob: 22e791e0026fa55b44a826ece41f9c7c3a7d5998 [file] [log] [blame]
// Copyright 2017 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 acl
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/luci/luci-go/common/data/stringset"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/common/retry/transient"
"github.com/luci/luci-go/scheduler/appengine/messages"
"github.com/luci/luci-go/server/auth"
"github.com/luci/luci-go/server/auth/identity"
"golang.org/x/net/context"
)
// GrantsByRole can answer questions who can READ and who OWNS the task.
type GrantsByRole struct {
Owners []string `gae:",noindex"`
Readers []string `gae:",noindex"`
}
func (g *GrantsByRole) IsOwner(c context.Context) (bool, error) {
return hasGrant(c, g.Owners, groupsAdministrators)
}
func (g *GrantsByRole) IsReader(c context.Context) (bool, error) {
if len(g.Readers) == 0 && len(g.Owners) == 0 {
// This is here for backwards compatiblity before ACLs were introduced.
// If Job doesn't specify READERs nor OWNERS explicitely, everybody can read.
// TODO(tAndrii): remove once every Job/Trigger has ACLs specified.
return true, nil
}
return hasGrant(c, g.Owners, g.Readers, groupsAdministrators)
}
func (g *GrantsByRole) Equal(o *GrantsByRole) bool {
eqSlice := func(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
return eqSlice(g.Owners, o.Owners) && eqSlice(g.Readers, o.Readers)
}
// AclSets are parsed and indexed `AclSet` of a project.
type AclSets map[string][]*messages.Acl
// ValidateAclSets validates list of AclSet of a project and returns AclSets.
func ValidateAclSets(sets []*messages.AclSet) (AclSets, error) {
as := make(AclSets, len(sets))
for _, s := range sets {
if s.Name == "" {
return nil, fmt.Errorf("missing 'name' field'")
}
if !aclSetNameRe.MatchString(s.Name) {
return nil, fmt.Errorf("%q is not valid value for 'name' field", s.Name)
}
if _, isDup := as[s.Name]; isDup {
return nil, fmt.Errorf("aclSet name %q is not unique", s.Name)
}
if len(s.GetAcls()) == 0 {
return nil, fmt.Errorf("aclSet %q has no entries", s.Name)
}
as[s.Name] = s.GetAcls()
}
return as, nil
}
// ValidateTaskAcls validates task's ACLs and returns TaskAcls.
func ValidateTaskAcls(pSets AclSets, tSets []string, tAcls []*messages.Acl) (*GrantsByRole, error) {
grantsLists := make([][]*messages.Acl, 0, 1+len(tSets))
if err := validateGrants(tAcls); err != nil {
return nil, err
}
grantsLists = append(grantsLists, tAcls)
for _, set := range tSets {
grantsList, exists := pSets[set]
if !exists {
return nil, fmt.Errorf("referencing AclSet '%s' which doesn't exist", set)
}
grantsLists = append(grantsLists, grantsList)
}
mg := mergeGrants(grantsLists...)
if n := len(mg.Owners) + len(mg.Readers); n > maxGrantsPerJob {
return nil, fmt.Errorf("Job or Trigger can have at most %d acls, but %d given", maxGrantsPerJob, n)
}
return mg, nil
}
////////////////////////////////////////////////////////////////////////////////
var (
// aclSetNameRe is used to validate AclSet Name field.
aclSetNameRe = regexp.MustCompile(`^[0-9A-Za-z_\-\.]{1,100}$`)
// maxGrantsPerJob is how many different grants are specified for a job.
maxGrantsPerJob = 32
groupsAdministrators = []string{"group:administrators"}
)
func validateGrants(gs []*messages.Acl) error {
for _, g := range gs {
switch {
case g.GetRole() != messages.Acl_OWNER && g.GetRole() != messages.Acl_READER:
return fmt.Errorf("invalid role %q", g.GetRole())
case g.GetGrantedTo() == "":
return fmt.Errorf("missing granted_to for role %s", g.GetRole())
case strings.HasPrefix(g.GetGrantedTo(), "group:"):
if g.GetGrantedTo()[len("group:"):] == "" {
return fmt.Errorf("invalid granted_to %q for role %s: needs a group name", g.GetGrantedTo(), g.GetRole())
}
default:
id := g.GetGrantedTo()
if !strings.ContainsRune(g.GetGrantedTo(), ':') {
id = "user:" + g.GetGrantedTo()
}
if _, err := identity.MakeIdentity(id); err != nil {
return errors.Annotate(err, "invalid granted_to %q for role %s", g.GetGrantedTo(), g.GetRole()).Err()
}
}
}
return nil
}
// mergeGrants merges valid grants into GrantsByRole, removing and sorting duplicates.
func mergeGrants(grantsLists ...[]*messages.Acl) *GrantsByRole {
all := map[messages.Acl_Role]stringset.Set{
messages.Acl_OWNER: stringset.New(maxGrantsPerJob),
messages.Acl_READER: stringset.New(maxGrantsPerJob),
}
for _, grantsList := range grantsLists {
for _, g := range grantsList {
all[g.GetRole()].Add(g.GetGrantedTo())
}
}
sortedSlice := func(s stringset.Set) []string {
r := s.ToSlice()
sort.Strings(r)
return r
}
return &GrantsByRole{
Owners: sortedSlice(all[messages.Acl_OWNER]),
Readers: sortedSlice(all[messages.Acl_READER]),
}
}
// hasGrant is current user is covered by any given grants.
func hasGrant(c context.Context, grantsList ...[]string) (bool, error) {
currentIdentity := auth.CurrentIdentity(c)
groups := []string{}
for _, grants := range grantsList {
for _, grant := range grants {
if strings.HasPrefix(grant, "group:") {
groups = append(groups, grant[len("group:"):])
continue
}
grantedIdentity := identity.Identity(grant)
if !strings.ContainsRune(grant, ':') {
// Just email.
grantedIdentity = identity.Identity("user:" + grant)
}
if grantedIdentity == currentIdentity {
return true, nil
}
}
}
if isMember, err := auth.IsMember(c, groups...); err != nil {
return false, transient.Tag.Apply(err)
} else {
return isMember, nil
}
}