| Index: scheduler/appengine/acl/acl.go
|
| diff --git a/scheduler/appengine/acl/acl.go b/scheduler/appengine/acl/acl.go
|
| index 32a4f3f94d13b0a176204996f4fe8d366f733db4..1a2cacc87d2c261cd52f03a35d1210fda3353d2c 100644
|
| --- a/scheduler/appengine/acl/acl.go
|
| +++ b/scheduler/appengine/acl/acl.go
|
| @@ -15,16 +15,180 @@
|
| package acl
|
|
|
| import (
|
| - "golang.org/x/net/context"
|
| + "context"
|
| + "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"
|
| )
|
|
|
| -func IsJobOwner(c context.Context, projectID, jobName string) bool {
|
| - // TODO(vadimsh): Do real ACLs.
|
| - ok, err := auth.IsMember(c, "administrators")
|
| - if err != nil {
|
| - panic(err)
|
| +// 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
|
| }
|
| - return ok
|
| }
|
|
|