blob: 87ed4f3a2ed5472ae013ace7473ccd98480bebe8 [file] [log] [blame]
// Copyright 2018 The Goma 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 account manages service account.
package account
import (
"context"
"io/ioutil"
"path/filepath"
"reflect"
"sync"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
)
// Account represents service account.
type Account interface {
// Equals compare account with other.
Equals(other Account) bool
// Token generates new oauth2 token for the account.
Token(ctx context.Context) (*oauth2.Token, error)
}
// Pool manages service accounts.
type Pool interface {
// New creates new account for name.
New(name string) (Account, error)
}
// JSONDir is a Pool with json files.
// It should be used for experiments only.
// we need to rotate keys.
// It uses application default credential, if "default" is requested.
// https://cloud.google.com/docs/authentication/production
type JSONDir struct {
Dir string
Scopes []string
}
type serviceAccount struct {
name string
config *jwt.Config
mu sync.Mutex
t *oauth2.Token
}
type defaultServiceAccount struct {
scopes []string
mu sync.Mutex
cred *google.Credentials
t *oauth2.Token
}
// New creates new account by loading json file in the dir.
// if name is "default", returns default service account instead.
func (j JSONDir) New(name string) (Account, error) {
if name == "default" {
return &defaultServiceAccount{scopes: j.Scopes}, nil
}
keyFile := filepath.Join(j.Dir, name+".json")
jsonKey, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, err
}
config, err := google.JWTConfigFromJSON(jsonKey, j.Scopes...)
if err != nil {
return nil, err
}
return &serviceAccount{
name: name,
config: config,
}, nil
}
// Equals checks other account has same name and config.
func (sa *serviceAccount) Equals(other Account) bool {
if other == nil {
return false
}
osa, ok := other.(*serviceAccount)
if !ok {
return false
}
if sa.name != osa.name {
return false
}
return reflect.DeepEqual(sa.config, osa.config)
}
// Token generates new oauth2 token.
func (sa *serviceAccount) Token(ctx context.Context) (*oauth2.Token, error) {
sa.mu.Lock()
defer sa.mu.Unlock()
if !sa.t.Valid() {
var err error
sa.t, err = sa.config.TokenSource(ctx).Token()
if err != nil {
return nil, err
}
}
return sa.t, nil
}
// Equals checks other account has same default service account.
func (sa *defaultServiceAccount) Equals(other Account) bool {
if other == nil {
return false
}
_, ok := other.(*defaultServiceAccount)
return ok
}
// Token generates new oauth2 token.
func (sa *defaultServiceAccount) Token(ctx context.Context) (*oauth2.Token, error) {
sa.mu.Lock()
defer sa.mu.Unlock()
if sa.cred == nil {
var err error
sa.cred, err = google.FindDefaultCredentials(ctx, sa.scopes...)
if err != nil {
return nil, err
}
}
if !sa.t.Valid() {
var err error
sa.t, err = sa.cred.TokenSource.Token()
if err != nil {
return nil, err
}
}
return sa.t, nil
}
// TODO: provide another account pool using SignJWT
// TODO: provide another account pool using luci-token-server.