Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(170)

Unified Diff: scheduler/appengine/acl/acl.go

Issue 2986033003: [scheduler]: ACLs phase 1 - per Job ACL specification and enforcement. (Closed)
Patch Set: Review. Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « scheduler/api/scheduler/v1/scheduler.pb.go ('k') | scheduler/appengine/acl/acl_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
}
« no previous file with comments | « scheduler/api/scheduler/v1/scheduler.pb.go ('k') | scheduler/appengine/acl/acl_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698