lib/tokensapi/handler.go (159 lines of code) (raw):
// Copyright 2020 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tokensapi
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/gorilla/mux" /* copybara-comment */
"google.golang.org/grpc/codes" /* copybara-comment */
"google.golang.org/grpc/status" /* copybara-comment */
"github.com/golang/protobuf/proto" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/handlerfactory" /* copybara-comment: handlerfactory */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */
epb "github.com/golang/protobuf/ptypes/empty" /* copybara-comment */
tpb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/tokens/v1" /* copybara-comment: go_proto */
)
var (
// tokenIDRE token_id part is base64 url encoded.
// base64 url encoding see: https://tools.ietf.org/html/rfc4648#section-5
tokenIDRE = regexp.MustCompile(`^(gcp|hydra):[0-9a-zA-Z-_]*$`)
// httpClient to call http request.
httpClient = http.DefaultClient
)
// Token is used in TokenProvider below.
type Token struct {
User string
RawTokenID string
TokenPrefix string
IssuedAt int64
ExpiresAt int64
Issuer string
Subject string
Audience string
Scope string
ClientID string
ClientName string
ClientUI map[string]string
Platform string
}
// TokenProvider includes methods for token management.
type TokenProvider interface {
ListTokens(ctx context.Context, user string, store storage.Store, tx storage.Tx) ([]*Token, error)
DeleteToken(ctx context.Context, user, tokenID string, store storage.Store, tx storage.Tx) error
TokenPrefix() string
}
func encodeTokenName(user, prefix, tokenID string) string {
return fmt.Sprintf("users/%s/tokens/%s:%s", user, prefix, base64.RawURLEncoding.EncodeToString([]byte(tokenID)))
}
// decodeTokenName splits token_id to token prefix and original tokne_id
func decodeTokenName(tokenID string) (string, string, error) {
ss := strings.Split(tokenID, ":")
if len(ss) != 2 {
return "", "", status.Errorf(codes.InvalidArgument, "token format invalid")
}
b, err := base64.RawURLEncoding.DecodeString(ss[1])
if err != nil {
return "", "", status.Errorf(codes.InvalidArgument, "token decode failed: %v", err)
}
return ss[0], string(b), nil
}
// ListTokensFactory creates a http handler for "/(identity|dam)/v1alpha/users/{user}/tokens"
// TODO should support filter parameters.
func ListTokensFactory(tokensPath string, providers []TokenProvider, store storage.Store) *handlerfactory.Options {
return &handlerfactory.Options{
TypeName: "token",
PathPrefix: tokensPath,
HasNamedIdentifiers: false,
Service: func() handlerfactory.Service {
return &listTokensHandler{
providers: providers,
store: store,
}
},
}
}
type listTokensHandler struct {
handlerfactory.Empty
providers []TokenProvider
store storage.Store
tx storage.Tx
}
func (s *listTokensHandler) Setup(r *http.Request, tx storage.Tx) (int, error) {
s.tx = tx
return http.StatusOK, nil
}
func (s *listTokensHandler) Get(r *http.Request, name string) (proto.Message, error) {
userID := mux.Vars(r)["user"]
resp := &tpb.ListTokensResponse{}
var err error
for _, p := range s.providers {
var l []*Token
// user may only have tokens on some provider.
// TODO: need original err from provider to check if it is a "not found" err.
l, err = p.ListTokens(r.Context(), userID, s.store, s.tx)
for _, t := range l {
resp.Tokens = append(resp.Tokens, toToken(t))
}
}
if len(resp.Tokens) > 0 {
return resp, nil
}
return nil, err
}
// toToken convert to pb Token
func toToken(t *Token) *tpb.Token {
return &tpb.Token{
Name: encodeTokenName(t.User, t.TokenPrefix, t.RawTokenID),
Issuer: t.Issuer,
Subject: t.Subject,
Audience: t.Audience,
ExpiresAt: t.ExpiresAt,
IssuedAt: t.IssuedAt,
Scope: t.Scope,
Client: &tpb.Client{
Id: t.ClientID,
Name: t.ClientName,
Ui: t.ClientUI,
},
Type: t.Platform,
}
}
// DeleteTokenFactory creates a http handler for "/(identity|dam)/v1alpha/users/{user}/tokens/{token_id}"
func DeleteTokenFactory(tokenPath string, providers []TokenProvider, store storage.Store) *handlerfactory.Options {
return &handlerfactory.Options{
TypeName: "token",
PathPrefix: tokenPath,
HasNamedIdentifiers: false,
NameChecker: map[string]*regexp.Regexp{
"token_id": tokenIDRE,
},
Service: func() handlerfactory.Service {
return &deleteTokenHandler{
providers: providers,
store: store,
}
},
}
}
type deleteTokenHandler struct {
handlerfactory.Empty
providers []TokenProvider
store storage.Store
tx storage.Tx
}
func (s *deleteTokenHandler) Setup(r *http.Request, tx storage.Tx) (int, error) {
s.tx = tx
return http.StatusOK, nil
}
func (s *deleteTokenHandler) Remove(r *http.Request, name string) (proto.Message, error) {
userID := mux.Vars(r)["user"]
tID := mux.Vars(r)["token_id"]
prefix, tokenID, err := decodeTokenName(tID)
if err != nil {
return nil, err
}
found := false
for _, p := range s.providers {
if p.TokenPrefix() == prefix {
found = true
err := p.DeleteToken(r.Context(), userID, tokenID, s.store, s.tx)
if err != nil {
return nil, err
}
}
}
if !found {
return nil, status.Errorf(codes.InvalidArgument, "unknown token platform: %s", prefix)
}
return &epb.Empty{}, nil
}