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

Side by Side Diff: go/src/infra/libs/auth/service.go

Issue 1153883002: go: infra/libs/* now live in luci-go. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: move the rest too Created 5 years, 7 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 unified diff | Download patch
« no previous file with comments | « go/src/infra/libs/auth/internal/user.go ('k') | go/src/infra/libs/auth/service_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package auth
6
7 import (
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io/ioutil"
12 "net/http"
13 "strings"
14 "time"
15
16 "infra/libs/logging"
17 )
18
19 // ServiceURL is URL of a service to talk to by default.
20 const ServiceURL string = "https://chrome-infra-auth.appspot.com"
21
22 // IdentityKind is enum like type with possible kinds of identities.
23 type IdentityKind string
24
25 const (
26 // IdentityKindUnknown is used when server return identity kind not reco gnized
27 // by the client.
28 IdentityKindUnknown IdentityKind = ""
29 // IdentityKindAnonymous is used to represent anonymous callers.
30 IdentityKindAnonymous IdentityKind = "anonymous"
31 // IdentityKindBot is used to represent bots. Identity name is bot's IP.
32 IdentityKindBot IdentityKind = "bot"
33 // IdentityKindService is used to represent AppEngine apps when they use
34 // X-Appengine-Inbound-AppId header to authenticate. Identity name is ap p ID.
35 IdentityKindService IdentityKind = "service"
36 // IdentityKindUser is used to represent end users or OAuth service acco unts.
37 // Identity name is an associated email (user email or service account e mail).
38 IdentityKindUser IdentityKind = "user"
39 )
40
41 var (
42 // ErrAccessDenied is returned by GroupsService methods if caller doesn' t have
43 // enough permissions to execute an action. Corresponds to 403 HTTP stat us.
44 ErrAccessDenied = errors.New("Access denied (HTTP 403)")
45
46 // ErrNoSuchItem is returned by GroupsService methods if requested item
47 // (e.g. a group) doesn't exist. Corresponds to 404 HTTP status.
48 ErrNoSuchItem = errors.New("No such item (HTTP 404)")
49 )
50
51 // Identity represents some caller that can make requests. It generalizes
52 // accounts of real people, bot accounts and service-to-service accounts.
53 type Identity struct {
54 // Kind describes what sort of identity this struct represents.
55 Kind IdentityKind
56 // Name defines concrete instance of identity, its meaning depends on ki nd.
57 Name string
58 }
59
60 // Group is a named list of identities, included subgroups and glob-like
61 // patterns (e.g. "user:*@example.com).
62 type Group struct {
63 // Name is a name of the group.
64 Name string
65 // Description is a human readable description of the group.
66 Description string
67 // Members is a list of identities included in the group explicitly.
68 Members []Identity
69 // Globs is a list of glob-like patterns for identities.
70 Globs []string
71 // Nested is a list of group names included into this group.
72 Nested []string
73 }
74
75 // String returns human readable representation of Identity.
76 func (i Identity) String() string {
77 switch i.Kind {
78 case IdentityKindUnknown:
79 return "unknown"
80 case IdentityKindAnonymous:
81 return "anonymous"
82 case IdentityKindUser:
83 return i.Name
84 default:
85 return fmt.Sprintf("%s:%s", i.Kind, i.Name)
86 }
87 }
88
89 // GroupsService knows how to talk to Groups API backend. Server side code
90 // is in https://github.com/luci/luci-py repository.
91 type GroupsService struct {
92 client *http.Client
93 serviceURL string
94 logger logging.Logger
95 }
96
97 // NewGroupsService constructs new instance of GroupsService that talks to given
98 // service URL via given http.Client. If url is empty string, the default
99 // backend will be used. If httpClient is nil, http.DefaultClient will be used.
100 func NewGroupsService(url string, client *http.Client, logger logging.Logger) *G roupsService {
101 if url == "" {
102 url = ServiceURL
103 }
104 if client == nil {
105 client = http.DefaultClient
106 }
107 if logger == nil {
108 logger = logging.Null()
109 }
110 return &GroupsService{
111 client: client,
112 serviceURL: url,
113 logger: logger,
114 }
115 }
116
117 // ServiceURL returns a string with root URL of a Groups backend.
118 func (s *GroupsService) ServiceURL() string {
119 return s.serviceURL
120 }
121
122 // FetchCallerIdentity returns caller's own Identity as seen by the server.
123 func (s *GroupsService) FetchCallerIdentity() (Identity, error) {
124 var response struct {
125 Identity string `json:"identity"`
126 }
127 err := s.doGet("/auth/api/v1/accounts/self", &response)
128 if err != nil {
129 return Identity{}, err
130 }
131 return parseIdentity(response.Identity)
132 }
133
134 // FetchGroup returns a group definition. It does not fetch nested groups.
135 // Returns ErrNoSuchItem error if no such group.
136 func (s *GroupsService) FetchGroup(name string) (Group, error) {
137 var response struct {
138 Group struct {
139 Name string `json:"name"`
140 Description string `json:"description"`
141 Members []string `json:"members"`
142 Globs []string `json:"globs"`
143 Nested []string `json:"nested"`
144 } `json:"group"`
145 }
146 err := s.doGet("/auth/api/v1/groups/"+name, &response)
147 if err != nil {
148 return Group{}, err
149 }
150 if response.Group.Name != name {
151 return Group{}, fmt.Errorf(
152 "Unexpected group name in server response: '%s', expecti ng '%s'",
153 response.Group.Name, name)
154 }
155 g := Group{
156 Name: response.Group.Name,
157 Description: response.Group.Description,
158 Members: make([]Identity, 0, len(response.Group.Members)),
159 Globs: response.Group.Globs,
160 Nested: response.Group.Nested,
161 }
162 for _, str := range response.Group.Members {
163 ident, err := parseIdentity(str)
164 if err != nil {
165 s.logger.Warningf("auth: failed to parse an identity in a group, ignoring it - %s", err)
166 } else {
167 g.Members = append(g.Members, ident)
168 }
169 }
170 return g, nil
171 }
172
173 // doGet sends GET HTTP requests and decodes JSON into response. It retries
174 // multiple times on transient errors.
175 func (s *GroupsService) doGet(path string, response interface{}) error {
176 if len(path) == 0 || path[0] != '/' {
177 return fmt.Errorf("Path should start with '/': %s", path)
178 }
179 url := s.serviceURL + path
180 for attempt := 0; attempt < 5; attempt++ {
181 if attempt != 0 {
182 s.logger.Warningf("auth: retrying request to %s", url)
183 sleep(2 * time.Second)
184 }
185 req, err := http.NewRequest("GET", url, nil)
186 if err != nil {
187 return err
188 }
189 resp, err := doRequest(s.client, req)
190 if err != nil {
191 return err
192 }
193 // Success?
194 if resp.StatusCode < 300 {
195 defer resp.Body.Close()
196 return json.NewDecoder(resp.Body).Decode(response)
197 }
198 // Fatal error?
199 if resp.StatusCode >= 300 && resp.StatusCode < 500 {
200 defer resp.Body.Close()
201 switch resp.StatusCode {
202 case 403:
203 return ErrAccessDenied
204 case 404:
205 return ErrNoSuchItem
206 default:
207 body, _ := ioutil.ReadAll(resp.Body)
208 return fmt.Errorf("Unexpected reply (HTTP %d):\n %s", resp.StatusCode, string(body))
209 }
210 }
211 // Retry.
212 resp.Body.Close()
213 }
214 return fmt.Errorf("Request to %s failed after 5 attempts", url)
215 }
216
217 // parseIdentity takes a string of form "<kind>:<name>" and returns Identity
218 // struct.
219 func parseIdentity(str string) (Identity, error) {
220 chunks := strings.Split(str, ":")
221 if len(chunks) != 2 {
222 return Identity{}, fmt.Errorf("Invalid identity string: '%s'", s tr)
223 }
224 kind := IdentityKind(chunks[0])
225 name := chunks[1]
226 switch kind {
227 case IdentityKindAnonymous:
228 if name != "anonymous" {
229 return Identity{}, fmt.Errorf("Invalid anonymous identit y: '%s'", str)
230 }
231 return Identity{
232 Kind: IdentityKindAnonymous,
233 Name: "anonymous",
234 }, nil
235 case IdentityKindBot, IdentityKindService, IdentityKindUser:
236 return Identity{
237 Kind: kind,
238 Name: name,
239 }, nil
240 default:
241 return Identity{}, fmt.Errorf("Unrecognized identity kind: '%s'" , str)
242 }
243 }
244
245 // sleep is mocked in tests.
246 var sleep = time.Sleep
247
248 // doRequest is mocked in tests.
249 var doRequest = func(c *http.Client, req *http.Request) (*http.Response, error) {
250 return c.Do(req)
251 }
OLDNEW
« no previous file with comments | « go/src/infra/libs/auth/internal/user.go ('k') | go/src/infra/libs/auth/service_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698