diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..55404683 --- /dev/null +++ b/api/api.go @@ -0,0 +1,136 @@ +package api + +import ( + "errors" + + "golang.org/x/net/context" + + "github.com/coreos/poke/api/apipb" + "github.com/coreos/poke/storage" +) + +// NewServer returns a gRPC server for talking to a storage. +func NewServer(s storage.Storage) apipb.StorageServer { + return &server{s} +} + +type server struct { + storage storage.Storage +} + +func fromPBClient(client *apipb.Client) storage.Client { + return storage.Client{ + ID: client.Id, + Secret: client.Secret, + RedirectURIs: client.RedirectUris, + TrustedPeers: client.TrustedPeers, + Public: client.Public, + Name: client.Name, + LogoURL: client.LogoUrl, + } +} + +func toPBClient(client storage.Client) *apipb.Client { + return &apipb.Client{ + Id: client.ID, + Secret: client.Secret, + RedirectUris: client.RedirectURIs, + TrustedPeers: client.TrustedPeers, + Public: client.Public, + Name: client.Name, + LogoUrl: client.LogoURL, + } +} + +func (s *server) CreateClient(ctx context.Context, req *apipb.CreateClientReq) (*apipb.CreateClientResp, error) { + // TODO(ericchiang): Create a more centralized strategy for creating client IDs + // and secrets which are restricted based on the storage. + client := fromPBClient(req.Client) + if client.ID == "" { + client.ID = storage.NewNonce() + } + if client.Secret == "" { + client.Secret = storage.NewNonce() + storage.NewNonce() + } + + if err := s.storage.CreateClient(client); err != nil { + return nil, err + } + return &apipb.CreateClientResp{Client: toPBClient(client)}, nil +} + +func (s *server) UpdateClient(ctx context.Context, req *apipb.UpdateClientReq) (*apipb.UpdateClientResp, error) { + switch { + case req.Id == "": + return nil, errors.New("no ID supplied") + case req.MakePublic && req.MakePrivate: + return nil, errors.New("cannot both make public and private") + case req.MakePublic && len(req.RedirectUris) != 0: + return nil, errors.New("redirect uris supplied for a public client") + } + + var client *storage.Client + updater := func(old storage.Client) (storage.Client, error) { + if req.MakePublic { + old.Public = true + } + if req.MakePrivate { + old.Public = false + } + if req.Secret != "" { + old.Secret = req.Secret + } + if req.Name != "" { + old.Name = req.Name + } + if req.LogoUrl != "" { + old.LogoURL = req.LogoUrl + } + if len(req.RedirectUris) != 0 { + if old.Public { + return old, errors.New("public clients cannot have redirect URIs") + } + old.RedirectURIs = req.RedirectUris + } + client = &old + return old, nil + } + + if err := s.storage.UpdateClient(req.Id, updater); err != nil { + return nil, err + } + return &apipb.UpdateClientResp{Client: toPBClient(*client)}, nil +} + +func (s *server) DeleteClient(ctx context.Context, req *apipb.DeleteClientReq) (*apipb.DeleteClientReq, error) { + if req.Id == "" { + return nil, errors.New("no client ID supplied") + } + if err := s.storage.DeleteClient(req.Id); err != nil { + return nil, err + } + return &apipb.DeleteClientReq{}, nil +} + +func (s *server) ListClients(ctx context.Context, req *apipb.ListClientsReq) (*apipb.ListClientsResp, error) { + clients, err := s.storage.ListClients() + if err != nil { + return nil, err + } + resp := make([]*apipb.Client, len(clients)) + for i, client := range clients { + resp[i] = toPBClient(client) + } + return &apipb.ListClientsResp{Clients: resp}, nil +} + +func (s *server) GetClient(ctx context.Context, req *apipb.GetClientReq) (*apipb.GetClientResp, error) { + if req.Id == "" { + return nil, errors.New("no client ID supplied") + } + client, err := s.storage.GetClient(req.Id) + if err != nil { + return nil, err + } + return &apipb.GetClientResp{Client: toPBClient(client)}, nil +} diff --git a/api/apipb/api.proto b/api/apipb/api.proto new file mode 100644 index 00000000..0930772a --- /dev/null +++ b/api/apipb/api.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +// Run `make grpc` at the top level directory to regenerate Go source code. + +package apipb; + +message Client { + string id = 1; + string secret = 2; + + repeated string redirect_uris = 3; + repeated string trusted_peers = 4; + + bool public = 5; + + string name = 6; + string logo_url = 7; +} + +message CreateClientReq { + Client client = 1; +} + +message CreateClientResp { + Client client = 1; +} + +message UpdateClientReq { + string id = 1; + + // Empty strings indicate that string fields should not be updated. + string secret = 2; + string name = 3; + string logo_url = 4; + + bool make_public = 5; + bool make_private = 6; + + // If no redirect URIs are specified, the current redirect URIs are preserved. + repeated string redirect_uris = 7; +} + +message UpdateClientResp { + Client client = 1; +} + +message ListClientsReq { +} + +message ListClientsResp { + repeated Client clients = 1; +} + +message DeleteClientReq { + string id = 1; +} + +message DeleteClientResp {} + +message GetClientReq { + string id = 1; +} + +message GetClientResp { + Client client = 1; +} + +service Storage { + rpc CreateClient(CreateClientReq) returns (CreateClientResp) {} + rpc DeleteClient(DeleteClientReq) returns (DeleteClientReq) {} + rpc GetClient(GetClientReq) returns (GetClientResp) {} + rpc ListClients(ListClientsReq) returns (ListClientsResp) {} + rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {} +} diff --git a/api/doc.go b/api/doc.go new file mode 100644 index 00000000..e91ea0aa --- /dev/null +++ b/api/doc.go @@ -0,0 +1,2 @@ +// Package api implements a gRPC interface for interacting with a storage. +package api diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index 3c38868c..9124f205 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -15,6 +15,9 @@ const keysName = "openid-connect-keys" // Client is a mirrored struct from storage with JSON struct tags and // Kubernetes type metadata. +// +// TODO(ericchiang): Kubernetes has an extremely restricted set of characters it can use for IDs. +// Consider base32ing client IDs. type Client struct { k8sapi.TypeMeta `json:",inline"` k8sapi.ObjectMeta `json:"metadata,omitempty"` diff --git a/storage/storage.go b/storage/storage.go index 55a99fef..376cdfec 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -77,13 +77,14 @@ func Open(driverName string, config map[string]string) (Storage, error) { type Storage interface { Close() error + // TODO(ericchiang): Let the storages set the IDs of these objects. CreateAuthRequest(a AuthRequest) error CreateClient(c Client) error CreateAuthCode(c AuthCode) error CreateRefresh(r Refresh) error // TODO(ericchiang): return (T, bool, error) so we can indicate not found - // requests that way. + // requests that way instead of using ErrNotFound. GetAuthRequest(id string) (AuthRequest, error) GetAuthCode(id string) (AuthCode, error) GetClient(id string) (Client, error)