Skip to content

Commit

Permalink
Merge pull request #16 from canonical/IAM-363-clients-api
Browse files Browse the repository at this point in the history
Add hydra service
  • Loading branch information
shipperizer authored Jul 19, 2023
2 parents d47faf3 + 15dc2b4 commit d575af0
Show file tree
Hide file tree
Showing 13 changed files with 1,786 additions and 2 deletions.
7 changes: 6 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"net/http"
"os"
"os/signal"
"strings"

"syscall"
"time"

ih "github.com/canonical/identity-platform-admin-ui/internal/hydra"
"github.com/kelseyhightower/envconfig"

"github.com/canonical/identity-platform-admin-ui/internal/config"
Expand All @@ -28,11 +30,14 @@ func main() {
}

logger := logging.NewLogger(specs.LogLevel, specs.LogFile)
debug := strings.ToLower(specs.LogLevel) == "debug"

monitor := prometheus.NewMonitor("identity-admin-ui", logger)
tracer := tracing.NewTracer(tracing.NewConfig(specs.TracingEnabled, specs.JaegerEndpoint, logger))

router := web.NewRouter(tracer, monitor, logger)
hClient := ih.NewClient(specs.HydraAdminURL, debug)

router := web.NewRouter(hClient, tracer, monitor, logger)

logger.Infof("Starting server on port %v", specs.Port)

Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/ory/hydra-client-go/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.16.0 // indirect
Expand All @@ -31,7 +35,10 @@ require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
90 changes: 90 additions & 0 deletions go.sum

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions internal/config/specs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ type EnvSpec struct {
LogFile string `envconfig:"log_file" default:"log.txt"`

Port int `envconfig:"port" default:"8080"`

HydraAdminURL string `envconfig:"hydra_admin_url" required:"true"`
}
30 changes: 30 additions & 0 deletions internal/hydra/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package hydra

import (
client "github.com/ory/hydra-client-go/v2"
)

type Client struct {
c *client.APIClient
}

func (c *Client) OAuth2Api() client.OAuth2Api {
return c.c.OAuth2Api
}

func NewClient(url string, debug bool) *Client {
c := new(Client)

configuration := client.NewConfiguration()

configuration.Debug = debug
configuration.Servers = []client.ServerConfiguration{
{
URL: url,
},
}

c.c = client.NewAPIClient(configuration)

return c
}
29 changes: 29 additions & 0 deletions internal/responses/responses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package responses

import "encoding/json"

type Response struct {
Data interface{} `json:"data"`
Message string `json:"message"`
Status int `json:"status"`
Links interface{} `json:"_links"`
Meta interface{} `json:"_meta"`
}

func (r *Response) PrepareResponse() ([]byte, error) {
resp, err := json.Marshal(r)
if err != nil {
return nil, err
}
return resp, err
}

func NewResponse(data interface{}, msg string, status int, links interface{}, meta interface{}) *Response {
r := new(Response)
r.Data = data
r.Message = msg
r.Status = status
r.Links = links
r.Meta = meta
return r
}
223 changes: 223 additions & 0 deletions pkg/clients/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package clients

import (
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"

"github.com/canonical/identity-platform-admin-ui/internal/logging"
"github.com/canonical/identity-platform-admin-ui/internal/responses"
"github.com/go-chi/chi/v5"
)

type API struct {
service ServiceInterface

logger logging.LoggerInterface
}

type PaginationLinksResponse struct {
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
Prev string `json:"prev,omitempty"`
Next string `json:"next,omitempty"`
}

func (a *API) RegisterEndpoints(mux *chi.Mux) {
mux.Get("/api/v0/clients", a.ListClients)
mux.Post("/api/v0/clients", a.CreateClient)
mux.Get("/api/v0/clients/{id}", a.GetClient)
mux.Put("/api/v0/clients/{id}", a.UpdateClient)
mux.Delete("/api/v0/clients/{id}", a.DeleteClient)
}

func (a *API) WriteJSONResponse(w http.ResponseWriter, data interface{}, msg string, status int, links interface{}, meta interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)

r := new(responses.Response)
r.Data = data
r.Message = msg
r.Status = status
r.Links = links
r.Meta = meta

err := json.NewEncoder(w).Encode(r)
if err != nil {
a.logger.Errorf("Unexpected error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

func (a *API) GetClient(w http.ResponseWriter, r *http.Request) {
clientId := chi.URLParam(r, "id")

res, e := a.service.GetClient(r.Context(), clientId)
if e != nil {
a.logger.Errorf("Unexpected error: %s", e)
a.WriteJSONResponse(w, nil, "Unexpected internal error", http.StatusInternalServerError, nil, nil)
return
}
if res.ServiceError != nil {
a.WriteJSONResponse(w, res.ServiceError, "Failed to get client", res.ServiceError.StatusCode, nil, nil)
return
}

a.WriteJSONResponse(w, res.Resp, "Client info", http.StatusOK, nil, nil)
}

func (a *API) DeleteClient(w http.ResponseWriter, r *http.Request) {
clientId := chi.URLParam(r, "id")

res, e := a.service.DeleteClient(r.Context(), clientId)
if e != nil {
a.logger.Errorf("Unexpected error: %s", e)
a.WriteJSONResponse(w, nil, "Unexpected internal error", http.StatusInternalServerError, nil, nil)
return
}
if res.ServiceError != nil {
a.WriteJSONResponse(w, res.ServiceError, "Failed to delete client", res.ServiceError.StatusCode, nil, nil)
return
}

a.WriteJSONResponse(w, "", "Client deleted", http.StatusOK, nil, nil)
}

func (a *API) CreateClient(w http.ResponseWriter, r *http.Request) {
// TODO @nsklikas: Limit request params?
json_data, err := io.ReadAll(r.Body)
if err != nil {
a.WriteJSONResponse(w, nil, "Failed to parse request body", http.StatusBadRequest, nil, nil)
return
}
c, err := a.service.UnmarshalClient(json_data)
if err != nil {
a.logger.Debugf("Failed to unmarshal JSON: %s", err)
a.WriteJSONResponse(w, nil, "Failed to parse request body", http.StatusBadRequest, nil, nil)
return
}

res, e := a.service.CreateClient(r.Context(), c)
if e != nil {
a.logger.Errorf("Unexpected error: %s", e)
a.WriteJSONResponse(w, nil, "Unexpected internal error", http.StatusInternalServerError, nil, nil)
return
}
if res.ServiceError != nil {
a.WriteJSONResponse(w, res.ServiceError, "Failed to create client", res.ServiceError.StatusCode, nil, nil)
return
}

a.WriteJSONResponse(w, res.Resp, "Created client", http.StatusCreated, nil, nil)
}

func (a *API) UpdateClient(w http.ResponseWriter, r *http.Request) {
clientId := chi.URLParam(r, "id")

json_data, err := io.ReadAll(r.Body)
if err != nil {
a.logger.Debugf("Failed to read response body: %s", err)
a.WriteJSONResponse(w, nil, "Failed to parse request body", http.StatusBadRequest, nil, nil)
return
}
// TODO @nsklikas: Limit request params?
c, err := a.service.UnmarshalClient(json_data)
if err != nil {
a.logger.Debugf("Failed to unmarshal JSON: %s", err)
a.WriteJSONResponse(w, nil, "Failed to parse request body", http.StatusBadRequest, nil, nil)
return
}
c.SetClientId(clientId)

res, e := a.service.UpdateClient(r.Context(), c)
if e != nil {
a.logger.Errorf("Unexpected error: %s", e)
a.WriteJSONResponse(w, nil, "Unexpected internal error", http.StatusInternalServerError, nil, nil)
return
}
if res.ServiceError != nil {
a.WriteJSONResponse(w, res.ServiceError, "Failed to update client", res.ServiceError.StatusCode, nil, nil)
return
}

a.WriteJSONResponse(w, res.Resp, "Updated client", http.StatusOK, nil, nil)
}

func (a *API) ListClients(w http.ResponseWriter, r *http.Request) {
req, err := a.parseListClientsRequest(r)
if err != nil {
a.WriteJSONResponse(w, nil, "Failed to parse request", http.StatusBadRequest, nil, nil)
return
}

res, e := a.service.ListClients(r.Context(), req)
if e != nil {
a.logger.Errorf("Unexpected error: %s", e)
a.WriteJSONResponse(w, nil, "Unexpected internal error", http.StatusInternalServerError, nil, nil)
return
}
if res.ServiceError != nil {
a.WriteJSONResponse(w, res.ServiceError, "Failed to fetch clients", res.ServiceError.StatusCode, nil, nil)
return
}

var links PaginationLinksResponse
if res.Links != nil {
links = PaginationLinksResponse{
First: a.convertLinkToUrl(res.Links.First, r.RequestURI),
Last: a.convertLinkToUrl(res.Links.Last, r.RequestURI),
Next: a.convertLinkToUrl(res.Links.Next, r.RequestURI),
Prev: a.convertLinkToUrl(res.Links.Prev, r.RequestURI),
}
}

a.WriteJSONResponse(w, res.Resp, "List of clients", http.StatusOK, links, res.Meta)
}

func (a *API) parseListClientsRequest(r *http.Request) (*ListClientsRequest, error) {
q := r.URL.Query()

cn := q.Get("client_name")
owner := q.Get("owner")
page := q.Get("page")
s := q.Get("size")

var size int = 200
if s != "" {
var err error
size, err = strconv.Atoi(s)
if err != nil {
return nil, err
}
}
return NewListClientsRequest(cn, owner, page, size), nil
}

func (a *API) convertLinkToUrl(l PaginationMeta, u string) string {
if l.Page == "" {
return ""
}
uu, err := url.Parse(u)
if err != nil {
a.logger.Fatal("Failed to parse URL: ", u)
}

q := uu.Query()
q.Set("page", l.Page)
q.Set("size", strconv.Itoa(l.Size))
uu.RawQuery = q.Encode()
return uu.String()
}

func NewAPI(service ServiceInterface, logger logging.LoggerInterface) *API {
a := new(API)

a.service = service

a.logger = logger

return a
}
Loading

0 comments on commit d575af0

Please sign in to comment.