| Index: go/src/infra/libs/auth/service.go
 | 
| diff --git a/go/src/infra/libs/auth/service.go b/go/src/infra/libs/auth/service.go
 | 
| deleted file mode 100644
 | 
| index 401be28baa4cff6b9bfe1631f5761afee938eb73..0000000000000000000000000000000000000000
 | 
| --- a/go/src/infra/libs/auth/service.go
 | 
| +++ /dev/null
 | 
| @@ -1,251 +0,0 @@
 | 
| -// Copyright 2015 The Chromium Authors. All rights reserved.
 | 
| -// Use of this source code is governed by a BSD-style license that can be
 | 
| -// found in the LICENSE file.
 | 
| -
 | 
| -package auth
 | 
| -
 | 
| -import (
 | 
| -	"encoding/json"
 | 
| -	"errors"
 | 
| -	"fmt"
 | 
| -	"io/ioutil"
 | 
| -	"net/http"
 | 
| -	"strings"
 | 
| -	"time"
 | 
| -
 | 
| -	"infra/libs/logging"
 | 
| -)
 | 
| -
 | 
| -// ServiceURL is URL of a service to talk to by default.
 | 
| -const ServiceURL string = "https://chrome-infra-auth.appspot.com"
 | 
| -
 | 
| -// IdentityKind is enum like type with possible kinds of identities.
 | 
| -type IdentityKind string
 | 
| -
 | 
| -const (
 | 
| -	// IdentityKindUnknown is used when server return identity kind not recognized
 | 
| -	// by the client.
 | 
| -	IdentityKindUnknown IdentityKind = ""
 | 
| -	// IdentityKindAnonymous is used to represent anonymous callers.
 | 
| -	IdentityKindAnonymous IdentityKind = "anonymous"
 | 
| -	// IdentityKindBot is used to represent bots. Identity name is bot's IP.
 | 
| -	IdentityKindBot IdentityKind = "bot"
 | 
| -	// IdentityKindService is used to represent AppEngine apps when they use
 | 
| -	// X-Appengine-Inbound-AppId header to authenticate. Identity name is app ID.
 | 
| -	IdentityKindService IdentityKind = "service"
 | 
| -	// IdentityKindUser is used to represent end users or OAuth service accounts.
 | 
| -	// Identity name is an associated email (user email or service account email).
 | 
| -	IdentityKindUser IdentityKind = "user"
 | 
| -)
 | 
| -
 | 
| -var (
 | 
| -	// ErrAccessDenied is returned by GroupsService methods if caller doesn't have
 | 
| -	// enough permissions to execute an action. Corresponds to 403 HTTP status.
 | 
| -	ErrAccessDenied = errors.New("Access denied (HTTP 403)")
 | 
| -
 | 
| -	// ErrNoSuchItem is returned by GroupsService methods if requested item
 | 
| -	// (e.g. a group) doesn't exist. Corresponds to 404 HTTP status.
 | 
| -	ErrNoSuchItem = errors.New("No such item (HTTP 404)")
 | 
| -)
 | 
| -
 | 
| -// Identity represents some caller that can make requests. It generalizes
 | 
| -// accounts of real people, bot accounts and service-to-service accounts.
 | 
| -type Identity struct {
 | 
| -	// Kind describes what sort of identity this struct represents.
 | 
| -	Kind IdentityKind
 | 
| -	// Name defines concrete instance of identity, its meaning depends on kind.
 | 
| -	Name string
 | 
| -}
 | 
| -
 | 
| -// Group is a named list of identities, included subgroups and glob-like
 | 
| -// patterns (e.g. "user:*@example.com).
 | 
| -type Group struct {
 | 
| -	// Name is a name of the group.
 | 
| -	Name string
 | 
| -	// Description is a human readable description of the group.
 | 
| -	Description string
 | 
| -	// Members is a list of identities included in the group explicitly.
 | 
| -	Members []Identity
 | 
| -	// Globs is a list of glob-like patterns for identities.
 | 
| -	Globs []string
 | 
| -	// Nested is a list of group names included into this group.
 | 
| -	Nested []string
 | 
| -}
 | 
| -
 | 
| -// String returns human readable representation of Identity.
 | 
| -func (i Identity) String() string {
 | 
| -	switch i.Kind {
 | 
| -	case IdentityKindUnknown:
 | 
| -		return "unknown"
 | 
| -	case IdentityKindAnonymous:
 | 
| -		return "anonymous"
 | 
| -	case IdentityKindUser:
 | 
| -		return i.Name
 | 
| -	default:
 | 
| -		return fmt.Sprintf("%s:%s", i.Kind, i.Name)
 | 
| -	}
 | 
| -}
 | 
| -
 | 
| -// GroupsService knows how to talk to Groups API backend. Server side code
 | 
| -// is in https://github.com/luci/luci-py repository.
 | 
| -type GroupsService struct {
 | 
| -	client     *http.Client
 | 
| -	serviceURL string
 | 
| -	logger     logging.Logger
 | 
| -}
 | 
| -
 | 
| -// NewGroupsService constructs new instance of GroupsService that talks to given
 | 
| -// service URL via given http.Client. If url is empty string, the default
 | 
| -// backend will be used. If httpClient is nil, http.DefaultClient will be used.
 | 
| -func NewGroupsService(url string, client *http.Client, logger logging.Logger) *GroupsService {
 | 
| -	if url == "" {
 | 
| -		url = ServiceURL
 | 
| -	}
 | 
| -	if client == nil {
 | 
| -		client = http.DefaultClient
 | 
| -	}
 | 
| -	if logger == nil {
 | 
| -		logger = logging.Null()
 | 
| -	}
 | 
| -	return &GroupsService{
 | 
| -		client:     client,
 | 
| -		serviceURL: url,
 | 
| -		logger:     logger,
 | 
| -	}
 | 
| -}
 | 
| -
 | 
| -// ServiceURL returns a string with root URL of a Groups backend.
 | 
| -func (s *GroupsService) ServiceURL() string {
 | 
| -	return s.serviceURL
 | 
| -}
 | 
| -
 | 
| -// FetchCallerIdentity returns caller's own Identity as seen by the server.
 | 
| -func (s *GroupsService) FetchCallerIdentity() (Identity, error) {
 | 
| -	var response struct {
 | 
| -		Identity string `json:"identity"`
 | 
| -	}
 | 
| -	err := s.doGet("/auth/api/v1/accounts/self", &response)
 | 
| -	if err != nil {
 | 
| -		return Identity{}, err
 | 
| -	}
 | 
| -	return parseIdentity(response.Identity)
 | 
| -}
 | 
| -
 | 
| -// FetchGroup returns a group definition. It does not fetch nested groups.
 | 
| -// Returns ErrNoSuchItem error if no such group.
 | 
| -func (s *GroupsService) FetchGroup(name string) (Group, error) {
 | 
| -	var response struct {
 | 
| -		Group struct {
 | 
| -			Name        string   `json:"name"`
 | 
| -			Description string   `json:"description"`
 | 
| -			Members     []string `json:"members"`
 | 
| -			Globs       []string `json:"globs"`
 | 
| -			Nested      []string `json:"nested"`
 | 
| -		} `json:"group"`
 | 
| -	}
 | 
| -	err := s.doGet("/auth/api/v1/groups/"+name, &response)
 | 
| -	if err != nil {
 | 
| -		return Group{}, err
 | 
| -	}
 | 
| -	if response.Group.Name != name {
 | 
| -		return Group{}, fmt.Errorf(
 | 
| -			"Unexpected group name in server response: '%s', expecting '%s'",
 | 
| -			response.Group.Name, name)
 | 
| -	}
 | 
| -	g := Group{
 | 
| -		Name:        response.Group.Name,
 | 
| -		Description: response.Group.Description,
 | 
| -		Members:     make([]Identity, 0, len(response.Group.Members)),
 | 
| -		Globs:       response.Group.Globs,
 | 
| -		Nested:      response.Group.Nested,
 | 
| -	}
 | 
| -	for _, str := range response.Group.Members {
 | 
| -		ident, err := parseIdentity(str)
 | 
| -		if err != nil {
 | 
| -			s.logger.Warningf("auth: failed to parse an identity in a group, ignoring it - %s", err)
 | 
| -		} else {
 | 
| -			g.Members = append(g.Members, ident)
 | 
| -		}
 | 
| -	}
 | 
| -	return g, nil
 | 
| -}
 | 
| -
 | 
| -// doGet sends GET HTTP requests and decodes JSON into response. It retries
 | 
| -// multiple times on transient errors.
 | 
| -func (s *GroupsService) doGet(path string, response interface{}) error {
 | 
| -	if len(path) == 0 || path[0] != '/' {
 | 
| -		return fmt.Errorf("Path should start with '/': %s", path)
 | 
| -	}
 | 
| -	url := s.serviceURL + path
 | 
| -	for attempt := 0; attempt < 5; attempt++ {
 | 
| -		if attempt != 0 {
 | 
| -			s.logger.Warningf("auth: retrying request to %s", url)
 | 
| -			sleep(2 * time.Second)
 | 
| -		}
 | 
| -		req, err := http.NewRequest("GET", url, nil)
 | 
| -		if err != nil {
 | 
| -			return err
 | 
| -		}
 | 
| -		resp, err := doRequest(s.client, req)
 | 
| -		if err != nil {
 | 
| -			return err
 | 
| -		}
 | 
| -		// Success?
 | 
| -		if resp.StatusCode < 300 {
 | 
| -			defer resp.Body.Close()
 | 
| -			return json.NewDecoder(resp.Body).Decode(response)
 | 
| -		}
 | 
| -		// Fatal error?
 | 
| -		if resp.StatusCode >= 300 && resp.StatusCode < 500 {
 | 
| -			defer resp.Body.Close()
 | 
| -			switch resp.StatusCode {
 | 
| -			case 403:
 | 
| -				return ErrAccessDenied
 | 
| -			case 404:
 | 
| -				return ErrNoSuchItem
 | 
| -			default:
 | 
| -				body, _ := ioutil.ReadAll(resp.Body)
 | 
| -				return fmt.Errorf("Unexpected reply (HTTP %d):\n%s", resp.StatusCode, string(body))
 | 
| -			}
 | 
| -		}
 | 
| -		// Retry.
 | 
| -		resp.Body.Close()
 | 
| -	}
 | 
| -	return fmt.Errorf("Request to %s failed after 5 attempts", url)
 | 
| -}
 | 
| -
 | 
| -// parseIdentity takes a string of form "<kind>:<name>" and returns Identity
 | 
| -// struct.
 | 
| -func parseIdentity(str string) (Identity, error) {
 | 
| -	chunks := strings.Split(str, ":")
 | 
| -	if len(chunks) != 2 {
 | 
| -		return Identity{}, fmt.Errorf("Invalid identity string: '%s'", str)
 | 
| -	}
 | 
| -	kind := IdentityKind(chunks[0])
 | 
| -	name := chunks[1]
 | 
| -	switch kind {
 | 
| -	case IdentityKindAnonymous:
 | 
| -		if name != "anonymous" {
 | 
| -			return Identity{}, fmt.Errorf("Invalid anonymous identity: '%s'", str)
 | 
| -		}
 | 
| -		return Identity{
 | 
| -			Kind: IdentityKindAnonymous,
 | 
| -			Name: "anonymous",
 | 
| -		}, nil
 | 
| -	case IdentityKindBot, IdentityKindService, IdentityKindUser:
 | 
| -		return Identity{
 | 
| -			Kind: kind,
 | 
| -			Name: name,
 | 
| -		}, nil
 | 
| -	default:
 | 
| -		return Identity{}, fmt.Errorf("Unrecognized identity kind: '%s'", str)
 | 
| -	}
 | 
| -}
 | 
| -
 | 
| -// sleep is mocked in tests.
 | 
| -var sleep = time.Sleep
 | 
| -
 | 
| -// doRequest is mocked in tests.
 | 
| -var doRequest = func(c *http.Client, req *http.Request) (*http.Response, error) {
 | 
| -	return c.Do(req)
 | 
| -}
 | 
| 
 |