lib/oathclients/endpoints.go (260 lines of code) (raw):
// Copyright 2019 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 oathclients
import (
"fmt"
"net/http"
"github.com/gorilla/mux" /* copybara-comment */
"google.golang.org/grpc/codes" /* copybara-comment */
"google.golang.org/grpc/status" /* copybara-comment */
"github.com/go-openapi/strfmt" /* copybara-comment */
"github.com/golang/protobuf/proto" /* copybara-comment */
"github.com/pborman/uuid" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/apis/hydraapi" /* copybara-comment: hydraapi */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/httputils" /* copybara-comment: httputils */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/hydra" /* copybara-comment: hydra */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */
pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/common/v1" /* copybara-comment: go_proto */
)
var (
// TODO: double check the default values.
defaultScope = "openid offline ga4gh_passport_v1 profile email identities account_admin"
defaultGrantTypes = []string{"authorization_code"}
defaultResponseTypes = []string{"token", "code", "id_token"}
)
// ClientService provides data storage for clients.
type ClientService interface {
HandlerSetup(tx storage.Tx, r *http.Request) (*ga4gh.Identity, int, error)
ClientByName(name string) *pb.Client
SaveClient(name, secret string, cli *pb.Client)
RemoveClient(name string, cli *pb.Client)
Save(tx storage.Tx, desc, typeName string, r *http.Request, id *ga4gh.Identity, m *pb.ConfigModification) error
CheckIntegrity(r *http.Request, m *pb.ConfigModification) *status.Status
}
//////////////////////////////////////////////////////////////////
// GET /identity/v1alpha/{realm}/clients/{name}
// GET /dam/v1alpha/{realm}/clients/{name}
// Return self client information
//////////////////////////////////////////////////////////////////
type clientHandler struct {
s ClientService
clientID string
item *pb.Client
id *ga4gh.Identity
}
// NewClientHandler returns clientHandler.
func NewClientHandler(s ClientService) *clientHandler {
return &clientHandler{s: s}
}
func (c *clientHandler) Setup(r *http.Request, tx storage.Tx) (int, error) {
if realm(r) != storage.DefaultRealm {
return http.StatusForbidden, status.Errorf(codes.PermissionDenied, "client api only allow on master realm")
}
clientID := ExtractClientID(r)
if len(clientID) == 0 {
return http.StatusBadRequest, fmt.Errorf("request requires clientID")
}
id, status, err := c.s.HandlerSetup(tx, r)
c.id = id
c.clientID = clientID
return status, err
}
func (c *clientHandler) LookupItem(r *http.Request, name string, vars map[string]string) bool {
clientID := ExtractClientID(r)
cli := c.s.ClientByName(name)
if cli != nil && cli.ClientId == clientID {
c.item = cli
return true
}
return false
}
func (c *clientHandler) NormalizeInput(r *http.Request, name string, vars map[string]string) error {
return nil
}
func (c *clientHandler) Get(r *http.Request, name string) (proto.Message, error) {
return &pb.ClientResponse{Client: c.item}, nil
}
func (c *clientHandler) Post(r *http.Request, name string) (proto.Message, error) {
return nil, fmt.Errorf("POST not allowed")
}
func (c *clientHandler) Put(r *http.Request, name string) (proto.Message, error) {
return nil, fmt.Errorf("PUT not allowed")
}
func (c *clientHandler) Patch(r *http.Request, name string) (proto.Message, error) {
return nil, fmt.Errorf("PATCH not allowed")
}
func (c *clientHandler) Remove(r *http.Request, name string) (proto.Message, error) {
return nil, fmt.Errorf("REMOVE not allowed")
}
func (c *clientHandler) CheckIntegrity(*http.Request) *status.Status {
return nil
}
func (c *clientHandler) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error {
// Accept, but do nothing.
return nil
}
//////////////////////////////////////////////////////////////////
// GET /identity/v1alpha/{realm}/config/clients/{name}:
// GET /dam/v1alpha/{realm}/config/clients/{name}:
// Return any given client information
// Require admin token
//
// POST /identity/v1alpha/{realm}/config/clients/{name}:
// POST /dam/v1alpha/{realm}/config/clients/{name}:
// Add given client in http body
// Require admin token
// Return added client information
//
// PATCH /identity/v1alpha/{realm}/config/clients/{name}:
// PATCH /dam/v1alpha/{realm}/config/clients/{name}:
// Update given client
// Require admin token
// Return any client information
//
// DELETE /identity/v1alpha/{realm}/config/clients/{name}:
// DELETE /dam/v1alpha/{realm}/config/clients/{name}:
// Delete given client
// Require admin token
// Return nothing
//////////////////////////////////////////////////////////////////
type adminClientHandler struct {
s ClientService
useHydra bool
httpClient *http.Client
hydraAdminURL string
input *pb.ConfigClientRequest
item *pb.Client
id *ga4gh.Identity
tx storage.Tx
}
// NewAdminClientHandler returns adminClientHandler
func NewAdminClientHandler(s ClientService, useHydra bool, httpClient *http.Client, hydraAdminURL string) *adminClientHandler {
return &adminClientHandler{s: s, useHydra: useHydra, httpClient: httpClient, hydraAdminURL: hydraAdminURL}
}
func (c *adminClientHandler) Setup(r *http.Request, tx storage.Tx) (int, error) {
if realm(r) != storage.DefaultRealm {
return http.StatusForbidden, status.Errorf(codes.PermissionDenied, "client api only allow on master realm")
}
id, status, err := c.s.HandlerSetup(tx, r)
c.id = id
c.tx = tx
return status, err
}
func (c *adminClientHandler) LookupItem(r *http.Request, name string, vars map[string]string) bool {
c.item = c.s.ClientByName(name)
return c.item != nil
}
func (c *adminClientHandler) NormalizeInput(r *http.Request, name string, vars map[string]string) error {
c.input = &pb.ConfigClientRequest{}
if err := httputils.DecodeProtoReq(c.input, r); err != nil {
return err
}
if c.input.Item == nil {
c.input.Item = &pb.Client{}
}
if c.input.Item.RedirectUris == nil {
c.input.Item.RedirectUris = []string{}
}
if c.input.Item.Ui == nil {
c.input.Item.Ui = make(map[string]string)
}
return nil
}
func (c *adminClientHandler) Get(r *http.Request, name string) (proto.Message, error) {
return &pb.ConfigClientResponse{Client: c.item}, nil
}
func (c *adminClientHandler) Post(r *http.Request, name string) (proto.Message, error) {
input := c.input.Item
if len(input.ClientId) == 0 {
input.ClientId = uuid.New()
}
if len(input.Scope) == 0 {
input.Scope = defaultScope
}
if len(input.GrantTypes) == 0 {
input.GrantTypes = defaultGrantTypes
}
if len(input.ResponseTypes) == 0 {
input.ResponseTypes = defaultResponseTypes
}
if err := CheckClientIntegrity(name, input); err != nil {
return nil, err
}
out := proto.Clone(input).(*pb.Client)
sec := uuid.New()
// Create the client on hydra.
if c.useHydra {
hyCli := toHydraClient(c.input.Item, name, sec, strfmt.NewDateTime())
resp, err := hydra.CreateClient(c.httpClient, c.hydraAdminURL, hyCli)
if err != nil {
return nil, err
}
out, sec = fromHydraClient(resp)
out.Ui = input.Ui
}
c.s.SaveClient(name, sec, out)
// Return the created client.
return &pb.ConfigClientResponse{
Client: out,
ClientSecret: sec,
}, nil
}
func (c *adminClientHandler) Put(r *http.Request, name string) (proto.Message, error) {
return nil, fmt.Errorf("PUT not allowed")
}
func (c *adminClientHandler) Patch(r *http.Request, name string) (proto.Message, error) {
// TODO should use field mask for update.
input := c.input.Item
if len(input.ClientId) == 0 {
input.ClientId = c.item.ClientId
}
if input.ClientId != c.item.ClientId {
return nil, fmt.Errorf("invalid client_id")
}
if len(input.Scope) == 0 {
input.Scope = c.item.Scope
}
if len(input.ResponseTypes) == 0 {
input.ResponseTypes = c.item.ResponseTypes
}
if len(input.GrantTypes) == 0 {
input.GrantTypes = c.item.GrantTypes
}
if len(input.RedirectUris) == 0 {
input.RedirectUris = c.item.RedirectUris
}
if len(input.Ui) == 0 {
input.Ui = c.item.Ui
}
if err := CheckClientIntegrity(name, input); err != nil {
return nil, err
}
out := proto.Clone(input).(*pb.Client)
sec := ""
if httputils.QueryParam(r, "rotate_secret") == "true" {
sec = uuid.New()
}
if c.useHydra {
hyCli := toHydraClient(input, name, sec, strfmt.NewDateTime())
resp, err := hydra.UpdateClient(c.httpClient, c.hydraAdminURL, hyCli.ClientID, hyCli)
if err != nil {
return nil, err
}
out, sec = fromHydraClient(resp)
out.Ui = input.Ui
}
c.s.SaveClient(name, sec, out)
// Return the updated client.
return &pb.ConfigClientResponse{
Client: out,
ClientSecret: sec,
}, nil
}
func (c *adminClientHandler) Remove(r *http.Request, name string) (proto.Message, error) {
if c.useHydra {
err := hydra.DeleteClient(c.httpClient, c.hydraAdminURL, c.item.ClientId)
if err != nil {
return nil, err
}
}
c.s.RemoveClient(name, c.item)
return nil, nil
}
func (c *adminClientHandler) CheckIntegrity(r *http.Request) *status.Status {
return c.s.CheckIntegrity(r, extractConfigModification(c.input))
}
func (c *adminClientHandler) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error {
return c.s.Save(c.tx, desc, typeName, r, c.id, extractConfigModification(c.input))
}
func extractConfigModification(input *pb.ConfigClientRequest) *pb.ConfigModification {
if input == nil {
return nil
}
return input.Modification
}
func toHydraClient(c *pb.Client, name, secret string, createdAt strfmt.DateTime) *hydraapi.Client {
return &hydraapi.Client{
Name: name,
ClientID: c.ClientId,
Secret: secret,
Scope: c.Scope,
GrantTypes: c.GrantTypes,
ResponseTypes: c.ResponseTypes,
RedirectURIs: c.RedirectUris,
CreatedAt: createdAt,
Audience: []string{c.ClientId},
}
}
func fromHydraClient(c *hydraapi.Client) (*pb.Client, string) {
return &pb.Client{
ClientId: c.ClientID,
Scope: c.Scope,
GrantTypes: c.GrantTypes,
ResponseTypes: c.ResponseTypes,
RedirectUris: c.RedirectURIs,
}, c.Secret
}
func realm(r *http.Request) string {
return mux.Vars(r)["realm"]
}